diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..084897a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(npm test)", + "Bash(npm run build)", + "Bash(git log:*)", + "Bash(npm test:*)", + "Bash(gh issue view:*)", + "WebSearch", + "Bash(gh pr view:*)", + "Bash(gh pr diff:*)", + "Bash(git add:*)", + "Bash(gh run watch:*)", + "Bash(node -e:*)", + "Bash(gh run list:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab0e23a..ac42c4e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,38 +2,125 @@ name: Build and test on: push: - branches: [main] + branches: [main, big-update] jobs: - build-and-deploy: + build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 22 + cache: npm + cache-dependency-path: src/package-lock.json - - name: Prepare + - name: Build and test + working-directory: src run: | - cd src - npm install + npm ci npm run build npm run test - cd dist - npm link - cd .. - cd .. - npm install -g @angular/cli - ng new your-angular-project --defaults - cd your-angular-project + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: src/dist + retention-days: 1 + + # Test ng add and ng deploy across multiple Angular versions (sequentially) + # This catches breaking changes in angular.json structure + # Each test creates its own git repo and deploys locally to verify the full flow + # + # Why checkout + npm ci? The ng-add.js requires @angular-devkit/core which must + # be resolvable from the dist folder. Node.js resolves modules by traversing up + # from the file's location, so we need src/node_modules available. + test-angular-versions: + needs: build-and-test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: src/package-lock.json + + - name: Install dependencies for module resolution + working-directory: src + run: npm ci + + - name: Configure git + run: | + git config --global user.name "CI Bot" + git config --global user.email "ci@test.local" + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: src/dist + + - name: Link angular-cli-ghpages + working-directory: src/dist + run: npm link + + # Test projects are created OUTSIDE the repo to get their own git repos + # (Angular CLI creates a git repo by default with --defaults) + # Each test creates a bare repo as a fake remote to deploy to + - name: Test Angular 18 + run: | + cd /tmp + git init --bare remote-18.git + npx @angular/cli@18 new test-app-18 --defaults + cd test-app-18 + git remote add origin /tmp/remote-18.git npm link angular-cli-ghpages - ng add angular-cli-ghpages + npx ng add angular-cli-ghpages + npx ng deploy + cd /tmp/remote-18.git && git branch | grep gh-pages + echo "Angular 18: build + deploy successful" - - name: Deploy - if: github.ref == 'refs/heads/main' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Test Angular 19 run: | - cd your-angular-project - ng deploy --name="The Buildbot" --email="buildbot@angular2buch.de" --cname=angular-cli-ghpages.angular.schule + cd /tmp + git init --bare remote-19.git + npx @angular/cli@19 new test-app-19 --defaults + cd test-app-19 + git remote add origin /tmp/remote-19.git + npm link angular-cli-ghpages + npx ng add angular-cli-ghpages + npx ng deploy + cd /tmp/remote-19.git && git branch | grep gh-pages + echo "Angular 19: build + deploy successful" + + - name: Test Angular 20 + run: | + cd /tmp + git init --bare remote-20.git + npx @angular/cli@20 new test-app-20 --defaults + cd test-app-20 + git remote add origin /tmp/remote-20.git + npm link angular-cli-ghpages + npx ng add angular-cli-ghpages + npx ng deploy + cd /tmp/remote-20.git && git branch | grep gh-pages + echo "Angular 20: build + deploy successful" + + - name: Test Angular 21 + run: | + cd /tmp + git init --bare remote-21.git + npx @angular/cli@21 new test-app-21 --defaults + cd test-app-21 + git remote add origin /tmp/remote-21.git + npm link angular-cli-ghpages + npx ng add angular-cli-ghpages + npx ng deploy + cd /tmp/remote-21.git && git branch | grep gh-pages + echo "Angular 21: build + deploy successful" diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ce69fb1..79b34c7 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -7,6 +7,13 @@ on: type: boolean default: true description: 'Dry-run (test without publishing)' + tag: + type: choice + default: 'latest' + options: + - latest + - next + description: 'npm tag (use "next" for pre-release/RC versions)' # Required for OIDC token exchange with npm (Trusted Publishers) permissions: @@ -33,14 +40,28 @@ jobs: npm run build npm test + - name: Show version info + working-directory: src/dist + run: | + VERSION=$(node -p "require('./package.json').version") + echo "πŸ“¦ Package: angular-cli-ghpages" + echo "πŸ“Œ Version: $VERSION" + echo "🏷️ Tag: ${{ inputs.tag }}" + echo "" + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "πŸ§ͺ Mode: DRY-RUN (no actual publish)" + else + echo "πŸš€ Mode: PUBLISH FOR REAL" + fi + - name: Publish to npm (dry-run) if: ${{ inputs.dry_run }} working-directory: src/dist run: | - echo "πŸ§ͺ DRY-RUN MODE" - echo "===============" echo "" - npm publish --provenance --dry-run + echo "===============" + npm publish --provenance --tag ${{ inputs.tag }} --dry-run + echo "===============" echo "" echo "βœ… Dry-run successful! Package is ready to publish." echo " To publish for real, run this workflow again with dry_run unchecked." @@ -49,9 +70,10 @@ jobs: if: ${{ !inputs.dry_run }} working-directory: src/dist run: | - echo "πŸš€ PUBLISHING TO NPM" - echo "====================" echo "" - npm publish --provenance + echo "===============" + npm publish --provenance --tag ${{ inputs.tag }} + echo "===============" echo "" echo "βœ… Published successfully with provenance!" + echo " https://www.npmjs.com/package/angular-cli-ghpages" diff --git a/.gitignore b/.gitignore index 8bfdd75..1268069 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ node_modules .bash_history *.swp *.swo -*.d.ts .idea *.classpath diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..44ed452 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,223 @@ +# CLAUDE.md + +This file provides guidance when working with code in this repository. + +## Overview + +`angular-cli-ghpages` is an Angular CLI builder/schematic that deploys Angular applications to GitHub Pages, Cloudflare Pages, or any Git repository. It wraps the `gh-pages` npm package and integrates with Angular CLI's deployment infrastructure via `ng deploy`. + +## Development Commands + +All development commands must be run from the `src` directory: + +```bash +cd src +``` + +**IMPORTANT:** The `src/.npmrc` file contains `ignore-scripts=false` to override global npm settings. **DO NOT DELETE OR MODIFY this file** - it's required for build scripts to run. + +### Build +```bash +npm run build +``` +Build process: `prebuild` (clean + regenerate schema.d.ts) β†’ `build` (tsc) β†’ `postbuild` (copy metadata to dist/). + +`schema.json` is source of truth for deployment options. Editing it requires rebuild. + +### Test +```bash +npm test +``` + +### Local Development with npm link + +For testing changes locally with an Angular project: + +1. Build and link from `src/dist`: + ```bash + cd src + npm run build + cd dist + npm link + ``` + +2. In your Angular test project: + ```bash + npm link angular-cli-ghpages + ng add angular-cli-ghpages + ng deploy --dry-run # Test without deploying + ``` + +### Debugging + +To debug the deploy builder in VSCode, use this `launch.json` configuration in your Angular project: + +```json +{ + "type": "node", + "request": "launch", + "name": "Debug ng deploy", + "skipFiles": ["/**"], + "program": "${workspaceFolder}/node_modules/@angular/cli/bin/ng", + "cwd": "${workspaceFolder}", + "sourceMaps": true, + "args": ["deploy", "--no-build"] +} +``` + +Alternatively, debug from command line: +```bash +node --inspect-brk ./node_modules/@angular/cli/bin/ng deploy +``` + +For debugging the standalone engine directly, use the "Launch Standalone Program" task in VSCode (configured in `.vscode/launch.json`). + +### Publishing + +Publishing uses [npm Trusted Publishers](https://docs.npmjs.com/trusted-publishers) with OIDC – no tokens stored in CI! + +1. Go to **Actions** β†’ **Publish to npm** +2. Click **Run workflow** β†’ select branch +3. Leave "Dry-run" checked to test, or uncheck for real publish +4. Wait for approval (5 min timer + required reviewer) + +Publishes with provenance attestation for supply chain security. + +For pre-release versions, after publishing: +```bash +npm dist-tag add angular-cli-ghpages@X.X.X-rc.X next +``` + +## Architecture + +### Entry Points + +1. **Angular CLI Integration** (`src/deploy/`): + - `builder.ts` - Angular builder entry point, called by `ng deploy` + - `actions.ts` - Orchestrates build and deployment process + - `schema.json` - Defines CLI options/arguments + +2. **Schematic** (`src/ng-add.ts`): + - Implements `ng add angular-cli-ghpages` + - Adds deploy target to `angular.json` + +3. **Standalone CLI** (`src/angular-cli-ghpages`): + - Bash script for non-Angular CLI usage + - Uses `commander` for CLI parsing + +4. **Core Engine** (`src/engine/`): + - `engine.ts` - Core deployment logic (wraps gh-pages) + - `defaults.ts` - Default configuration values + +### Deployment Flow + +``` +ng deploy + ↓ +builder.ts (createBuilder) + ↓ +actions.ts (deploy function) + β”œβ”€β†’ Build Angular app (if not --no-build) + β”‚ Uses BuilderContext.scheduleTarget() + └─→ engine.run() + β”œβ”€β†’ Prepare options (tokens, CI env vars) + β”œβ”€β†’ Create .nojekyll file (bypasses Jekyll on GitHub) + β”œβ”€β†’ Create 404.html (copy of index.html for SPAs) + β”œβ”€β†’ Create CNAME file (if custom domain) + └─→ Publish via gh-pages package +``` + +### Build Target Resolution + +**Precedence:** +1. `prerenderTarget` - For SSG/prerendering builds (if specified, overrides all others) +2. `buildTarget` - Standard build target (if specified) +3. Default - `${project}:build:production` + +**Implementation details:** +- Static build target: `buildTarget || default` (see `src/deploy/builder.ts`) +- Final target: `prerenderTarget || staticBuildTarget` (see `src/deploy/builder.ts`) + +Output directory resolution: +- Checks `angular.json` for `outputPath` +- If string: appends `/browser` (modern Angular convention) +- If object: uses `${base}/${browser}` properties +- Can be overridden with `--dir` option + +### Token Injection + +The engine automatically injects authentication tokens into HTTPS repository URLs: + +1. Discovers remote URL from current git repo (if `--repo` not specified) +2. Checks environment variables in order: `GH_TOKEN`, `PERSONAL_TOKEN`, `GITHUB_TOKEN` +3. Transforms: `https://github.com/...` β†’ `https://x-access-token:TOKEN@github.com/...` + +**Note:** Tokens only work with HTTPS, not SSH URLs (git@github.com). + +### CI Environment Detection + +The engine appends CI metadata to commit messages when running on: +- Travis CI (`TRAVIS` env var) +- CircleCI (`CIRCLECI` env var) +- GitHub Actions (`GITHUB_ACTIONS` env var) + +### Option Name Mapping + +**CRITICAL:** Angular CLI passes `--no-X` flags as `noX: true`, NOT as `X: false`. The engine must manually invert these: + +- `--no-dotfiles` β†’ Angular passes `{ noDotfiles: true }` β†’ Engine converts to `{ dotfiles: false }` +- `--no-notfound` β†’ Angular passes `{ noNotfound: true }` β†’ Engine converts to `{ notfound: false }` +- `--no-nojekyll` β†’ Angular passes `{ noNojekyll: true }` β†’ Engine converts to `{ nojekyll: false }` + +## Important Conventions + +1. **No Server-Side Rendering**: GitHub Pages only supports static files. SSR/Universal build targets are not supported. + +2. **404.html Handling**: + - **GitHub Pages**: Requires `404.html` workaround (only way to get SPA routing, but returns HTTP 404 status) + - **Cloudflare Pages**: MUST NOT have `404.html` - its presence disables native SPA mode + - **Future**: Consider changing default or auto-detecting deployment target + +3. **Jekyll Bypass**: Creates `.nojekyll` to prevent GitHub Pages from processing files through Jekyll (which would break files starting with `_` or `.txt` in assets). + +4. **Breaking Changes in v2**: Changed from guessing build conventions in Angular 17+. Projects may need explicit `--build-target` specification. + +## Deprecated Options (Maintainers Only) + +**Current deprecated options:** +- `noSilent` - Ignored with warning + +**Rejected options (v2.1+):** +- `browserTarget` - Actively rejected with clear error message ("Use buildTarget instead"). Not silently ignored - users get explicit feedback. + +## Testing + +- Uses Jest (`npm test`), tests in `*.spec.ts` files +- Requires git clone with `origin` remote (see `test-prerequisites.spec.ts`) +- All tests preserve/restore `process.env` using `originalEnv` pattern +- **No test counts in documentation** - they become stale quickly and are bragging + +### Testing Rules + +1. Use `.toBe()` for scalar equality (strings, numbers, booleans) +2. Use `.toContain()` for array membership or substring checks in long messages +3. Variable reuse for passthrough (same var = no transformation) +4. Separate variables for transformations (input != expected) + +**Avoid:** +- `.toContain('partial')` when you could use `.toBe(fullExpectedValue)` +- Reusing literals instead of variables to show intent + +### TypeScript: No `any` Type + +Use proper types, `unknown`, or `Partial` instead. For mocks: `Partial` cast to `CompleteType`. + +## Related Projects + +For sync considerations with AngularFire, monitor: +- https://github.com/angular/angularfire/blob/master/src/schematics/deploy/builder.ts +- https://github.com/angular/angularfire/blob/master/src/schematics/deploy/actions.ts + +## GitHub CLI Usage + +When performing GitHub operations (creating issues, PRs, etc.), use the `gh` CLI tool instead of web requests to avoid rate limiting and authentication issues. diff --git a/LICENSE b/LICENSE index d214ce3..22ea508 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017-2021 Johannes Hoppe +Copyright (c) 2017-2026 Angular.Schule (by Johannes Hoppe) Copyright (c) 2019 Minko Gechev Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/README.md b/README.md index a98a1a2..97408ca 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ 1. [πŸ“– Changelog](#changelog) 2. [⚠️ Prerequisites](#prerequisites) -3. [πŸš€ Quick Start (local development)](#quickstart-local) +3. [πŸš€ Quick Start](#quickstart) 4. [βš™οΈ Installation](#installation) 5. [πŸš€ Continuous Delivery](#continuous-delivery) 6. [πŸ“¦ Deployment Options](#options) - [--base-href](#base-href) - [--build-target](#build-target) + - [--prerender-target](#prerender-target) - [--no-build](#no-build) - [--repo](#repo) - [--message](#message) @@ -28,10 +29,10 @@ - [--no-nojekyll](#no-nojekyll) - [--cname](#cname) - [--add](#add) + - [--dir](#dir) - [--dry-run](#dry-run) 7. [πŸ“ Configuration File](#configuration-file) -8. [🌍 Environments](#environments) -9. [⁉️ FAQ](#faq) +8. [πŸ…°οΈ About](#about)
@@ -39,76 +40,30 @@ A detailed changelog is available in the [releases](https://github.com/angular-schule/angular-cli-ghpages/releases) section. -**⚠️ BREAKING CHANGE (v2)** - -The internal build of Angular has changed with Angular 17. -Unfortunately, there are now a lot of different _build targets_ and builders. -We will try to guess the correct build target, based on the usual conventions to name them. -The conventions are shown below, try to specify the build target more and more explicitly until the project compiles. - -In the following example, your app is called `test` and you want to deploy the `production` build. - -```bash -ng deploy -``` - -If this doesn't work, try this: - -```bash -ng deploy --build-target=test -``` - -If this doesn't work, try this: - -```bash -ng deploy --build-target=test:build:production -``` - -You can also and modify your `angular.json` to archive the same: - -```json -{ - "deploy": { - "builder": "angular-cli-ghpages:deploy", - "options": { - "buildTarget": "test:build:production" - } - } -} -``` - -For your convenience, you can also use `prerenderTarget` (which adds the suffix `:prerender:production`). -There is no support for `universalBuildTarget` or `serverTarget` because GitHub Pages only supports static assets and no Server-Side Rendering! - -We will then try to deploy the `dist/test/browser` folder to GitHub Pages. -If this is not the folder that you want to serve, you should explicitly specify the directory with the `--dir` option: - -```bash -ng deploy --dir=dist/test/browser -``` - -This new build logic is a breaking change, therefore `angular-cli-ghpages` v2 only supports Angular 17 and higher. -For previous versions of Angular, use `angular-cli-ghpages` v1.x. +`angular-cli-ghpages` v3 supports Angular 18 to 21. +For previous versions of Angular, use v1 or v2. ## ⚠️ Prerequisites This command has the following prerequisites: - Git 1.9 or higher (execute `git --version` to check your version) -- Angular project created via [Angular CLI](https://github.com/angular/angular-cli) v17 or greater -- older Angular projects can still use a v1.x version or use the standalone program. See the documentation at [README_standalone](https://github.com/angular-schule/angular-cli-ghpages/blob/master/docs/README_standalone.md). +- Angular project created via [Angular CLI](https://github.com/angular/angular-cli) v18 or greater +- Older Angular projects can still use a v1.x version or use the standalone program. See the documentation at [README_standalone](https://github.com/angular-schule/angular-cli-ghpages/blob/master/docs/README_standalone.md). -## πŸš€ Quick Start (local development) +## πŸš€ Quick Start + +`angular-cli-ghpages` compiles your app, then pushes the build output to a dedicated branch (default: `gh-pages`) – all with a single command: `ng deploy`. This branch serves as the source for your web host and works out of the box with GitHub Pages and Cloudflare Pages. This quick start assumes that you are starting from scratch. -If you already have an existing Angular project on GitHub, skip step 1 and 2. +If you already have an existing Angular project on GitHub, skip steps 1 and 2. 1. Install the latest version of the Angular CLI globally and create a new Angular project. ```sh - npm install -g @angular/cli - ng new your-angular-project --defaults + npm install --location=global @angular/cli + ng new your-angular-project cd your-angular-project ``` @@ -126,23 +81,29 @@ If you already have an existing Angular project on GitHub, skip step 1 and 2. - Please enter the URL `https://github.com//.git` into your browser – you should see your existing repository on GitHub. - Please double-check that you have the necessary rights to make changes to the given project! -3. Add `angular-cli-ghpages` to your project. For details, see the [installation section](#installation). +3. Add `angular-cli-ghpages` to your project. When you run `ng deploy` for the first time, the Angular CLI will prompt you to choose a deployment target – select **GitHub Pages**: ```sh - ng add angular-cli-ghpages + ng deploy ``` -4. Deploy your project to GitHub pages with all default settings. - Your project will be automatically built in production mode. - - ```sh - ng deploy --base-href=// ``` + Would you like to add a package with "deploy" capabilities now? + No + Amazon S3 + Firebase + Netlify + ❯ GitHub Pages + + ↑↓ navigate β€’ ⏎ select + ``` + + Alternatively, you can install it directly via `ng add angular-cli-ghpages`. See the [installation section](#installation) for details. - Which is the same as: +4. After the installation, the same `ng deploy` command will build and deploy your project. Your project will be automatically built in production mode. ```sh - ng deploy your-angular-project --base-href=// + ng deploy --base-href=// ``` Please be aware of the `--base-href` option. It is necessary when your project will be deployed to a non-root folder. See more details below. @@ -190,7 +151,7 @@ ng deploy --repo=https://github.com//.git --name="Your > **ℹ️ Note for GitHub Actions** > -> The `GITHUB_TOKEN` (installation access token) will only trigger a release of a new website if the action runs in a private repository. In a public repo, a commit is generated, but the site does not change. See this [GitHub Community post](https://github.community/t5/GitHub-Actions/Github-action-not-triggering-gh-pages-upon-push/m-p/26869) for more info. If your repo is public, you must still use the `GH_TOKEN` (personal access token). +> The `GITHUB_TOKEN` (installation access token) will only trigger a release of a new website if the action runs in a private repository. In a public repo, a commit is generated, but the site does not change. If your repo is public, you must still use the `GH_TOKEN` (personal access token). ## πŸ“¦ Deployment Options @@ -209,7 +170,7 @@ Same as `ng build --base-href=/XXX/` ##### A) You don't want to use a custom domain -If you don't want to use an own domain, then your later URL of your hosted Angular project should look like this: +If you don't want to use your own domain, the URL of your hosted Angular project will look like this: `https://your-username.github.io/the-repositoryname`. In this case you have to adjust the `--base-href` accordingly: @@ -238,7 +199,7 @@ See the option [--cname](#cname) for more information! If no `buildTarget` is set, the `production` build of the default project will be chosen. The `buildTarget` simply points to an existing build configuration for your project, as specified in the `configurations` section of `angular.json`. -Most projects have a default configuration and a production configuration (commonly activated by using the `--prod` flag) but it is possible to specify as many build configurations as needed. +Most projects have a default configuration and a production configuration (commonly activated by using the `--configuration production` option) but it is possible to specify as many build configurations as needed. This is equivalent to calling the command `ng build --configuration=XXX`. This command has no effect if the option `--no-build` is active. @@ -259,13 +220,22 @@ NOW: ng deploy --build-target=test ``` +#### --prerender-target + +- **optional** +- Default: `undefined` + +Specifies the Angular architect target to use for prerendering instead of buildTarget. + +**Target Precedence:** If `prerenderTarget` is specified, it takes precedence over `buildTarget`. This option has no effect if `--no-build` is active. + #### --no-build - **optional** -- Default: `false` (string) +- Default: `false` (boolean) - Example: - - `ng deploy` – Angular project is build in production mode before the deployment - - `ng deploy --no-build` – Angular project is NOT build + - `ng deploy` – Angular project is built in production mode before the deployment + - `ng deploy --no-build` – Angular project is NOT built Skip the build process during deployment. This can be used when you are sure that you haven't changed anything and want to deploy with the latest artifact. @@ -286,7 +256,7 @@ you can provide the repository URL in the `repo` option. > **ℹ️ Hint** > -> Set an environment variable with the name `GH_TOKEN` / `PERSONAL_TOKEN` or `GITHUB_TOKEN` and it will be automatically added to the URL, if it uses the HTTPS shema (it must start with `https://github.com`). +> Set an environment variable with the name `GH_TOKEN` / `PERSONAL_TOKEN` or `GITHUB_TOKEN` and it will be automatically added to the URL, if it uses the HTTPS schema (it must start with `https://github.com`). > Tokens are generally not supported for Git over SSH (starts with `git@github.com`). Learn more about ["personal access tokens" here](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) (`GH_TOKEN`) and about the ["installation access token" here](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token) (`GITHUB_TOKEN`). `PERSONAL_TOKEN` is an alias for `GH_TOKEN`. @@ -298,7 +268,7 @@ Learn more about ["personal access tokens" here](https://help.github.com/article - Example: `ng deploy --message="What could possibly go wrong?"` The commit message **must be wrapped in quotes** if there are any spaces in the text. -Some additional text is always added to the message, if the command runs on Travis CI, Circle CI or GitHub Actions. +Some additional text is always added to the message, if the command runs on Travis CI, CircleCI or GitHub Actions. #### --branch @@ -309,7 +279,7 @@ Some additional text is always added to the message, if the command runs on Trav The name of the branch you'll be pushing to. The default uses GitHub's `gh-pages` branch, but this can be configured to push to any branch on any remote. -You have to change this to `master` if you are pushing to a GitHub organization page (instead of a GitHub user page). +You may need to change this to `main` (or `master` for older repositories) if you are pushing to a GitHub organization page (instead of a GitHub user page). #### --name & --email @@ -341,9 +311,12 @@ With `--no-dotfiles` files starting with `.` are ignored. - `ng deploy` – A `404.html` file is created by default. - `ng deploy --no-notfound` – No `404.html` file is created. -By default, a `404.html` file is created, because this is the only known workaround to avoid 404 error messages on GitHub Pages. -For Cloudflare Pages we highly recommend to disable the `404.html` file by setting this switch to true! -See [#178](https://github.com/angular-schule/angular-cli-ghpages/issues/178) +By default, a `404.html` file is created, because this is the only known workaround for SPA routing on GitHub Pages (note: GitHub still returns HTTP 404 status, which may affect Brave browser and social previews). + +> [!IMPORTANT] +> **Cloudflare Pages users:** You **must** use `--no-notfound` to enable native SPA routing. Cloudflare Pages only activates SPA mode when no `404.html` file exists. See [#178](https://github.com/angular-schule/angular-cli-ghpages/issues/178) +> +> We plan to change the default behavior in a future release to better support Cloudflare Pages. #### --no-nojekyll @@ -379,8 +352,8 @@ A CNAME file will be created enabling you to use a custom domain. - Example: - `ng deploy --add=true` -If is set to `true`, it will only add, and never remove existing files. -By default, existing files in the target branch are removed before adding the ones. +If it is set to `true`, it will only add, and never remove existing files. +By default, existing files in the target branch are removed before adding new ones. [More information](https://www.npmjs.com/package/gh-pages#optionsadd). #### --dir @@ -405,11 +378,14 @@ This can be very useful because it outputs what would happen without doing anyth ## πŸ“ Configuration File -To avoid all these command-line cmd options, you can write down your configuration in the `angular.json` file in the `options` attribute of your deploy project's architect. Just change the kebab-case to lower camel case. This is the notation of all options in lower camel case: +To avoid repeating command-line options, you can configure them in your `angular.json` file under the `options` key of your deploy configuration. Use camelCase instead of kebab-case. Available options: + - baseHref - buildTarget +- prerenderTarget - noBuild +- remote - repo - message - branch @@ -419,6 +395,7 @@ To avoid all these command-line cmd options, you can write down your configurati - noNotfound - noNojekyll - cname +- add - dir - dryRun @@ -443,39 +420,24 @@ becomes } ``` -Now you can just run `ng deploy` without all the options in the command line! πŸ˜„ +Now you can just run `ng deploy` without all the options in the command line! > **ℹ️ Hint** > > You can always use the [--dry-run](#dry-run) option to verify if your configuration is right. > The project will build but not deploy. -## 🌍 Environments - -We have seen `angular-cli-ghpages` running on various environments, like Travis CI, CircleCi or GitHub Actions. -Please share your knowledge by writing an article about how to set up the deployment. +## πŸ…°οΈ About -1. [GitHub Actions](https://angular.schule/blog/2020-01-everything-github) -2. Travis CI -3. CircleCI +![Angular.Schule](https://assets.angular.schule/header-only-logo.png) -## ⁉️ FAQ +πŸ‡¬πŸ‡§ `angular-cli-ghpages` is brought to you by the **Angular.Schule** team – two Google Developer Experts (GDE) from Germany. We get you and your team up to speed with Angular through remote trainings in English! Visit [angular.schule](https://angular.schule) to learn more. -Before posting any issue, [please read the FAQ first](https://github.com/angular-schule/angular-cli-ghpages/wiki/FAQ). -See the contributors documentation at [README_contributors](https://github.com/angular-schule/angular-cli-ghpages/blob/master/docs/README_contributors.md) if you want to debug and test this project. - -## License - -Code released under the [MIT license](LICENSE). - -
+πŸ‡©πŸ‡ͺ `angular-cli-ghpages` wurde fΓΌr Sie entwickelt von der **Angular.Schule**! Wir machen Sie und Ihr Team fit fΓΌr das Webframework Angular – in [offenen Gruppen](https://angular.schule/schulungen/online) oder [individuellen Inhouse-Schulungen](https://angular.schule/schulungen/online-teams). Von den Buchautoren und Google Developer Experts (GDE) Johannes Hoppe und Ferdinand Malcher. -### © 2017-2024 https://angular.schule - -This project is made on top of [tschaub/gh-pages](https://github.com/tschaub/gh-pages). -Thank you very much for this great foundation! +Β© 2017-2026 [npm-url]: https://www.npmjs.com/package/angular-cli-ghpages -[npm-image]: https://badge.fury.io/js/angular-cli-ghpages.svg +[npm-image]: https://img.shields.io/npm/v/angular-cli-ghpages.svg diff --git a/docs/README_contributors.md b/docs/README_contributors.md index a9c8c63..9ce7a6d 100644 --- a/docs/README_contributors.md +++ b/docs/README_contributors.md @@ -21,6 +21,8 @@ npm run build npm test ``` +**Test Prerequisites**: Tests must run from a git clone with `origin` remote configured. See `src/test-prerequisites.spec.ts` for validation details. + ## Local development If you want to try the latest package locally without installing it from NPM, use the following instructions. @@ -31,16 +33,21 @@ Follow the instructions for [checking and updating the Angular CLI version](#ang ### 1. Optional: Latest Angular version This builder requires the method `getTargetOptions()` from the Angular DevKit which was introduced [here](https://github.com/angular/angular-cli/pull/13825/files). -All Angular projects with Angular 9 and greater are supposed to be compatible. (Actually it works with some versions of 8.x too, but you want to be up to date anyway, don't you?) -Execute the next three steps, if your test project is still older. + +**Version compatibility:** +- **v3.x:** Supports Angular 18 and higher (current version) +- **v2.x:** Supported Angular 18 (previous major version) +- **v1.x:** Supported Angular 9-17 (deprecated) + +Execute the next three steps to update your test project to the latest Angular version. 1. Install the latest version of the Angular CLI. ```sh - npm install -g @angular/cli + npm install --location=global @angular/cli ``` -2. Run `ng version`, to make sure you have installed Angular v17 or greater. +2. Run `ng version` to make sure you have installed Angular v18 or greater. 3. Update your existing project using the command: @@ -151,6 +158,10 @@ cd angular-cli-ghpages/src npm test ``` +**Environment Requirements:** + +Some tests (remote URL discovery and `getRemoteUrl` integration tests) expect to run inside a real git clone of this repository with an `origin` remote configured. Running tests from a zip file or bare copy without `.git` is not supported and will cause test failures. + ### 5. Debugging To debug angular-cli-ghpages you need to: @@ -177,14 +188,99 @@ Use VSCode and debug the task `Launch Standalone Program`. ## Publish to NPM +Publishing uses [npm Trusted Publishers](https://docs.npmjs.com/trusted-publishers) with OIDC – no long-lived tokens needed! πŸ” + +1. Go to **Actions** β†’ **Publish to npm** +2. Click **Run workflow** β†’ select branch (usually `main`) +3. Leave "Dry-run" checked to test, or uncheck for real publish +4. Wait for approval (5 min timer + required reviewer) + +The workflow builds, tests, and publishes with [provenance attestation](https://docs.npmjs.com/generating-provenance-statements). + +For pre-release versions, after publishing: +```bash +npm dist-tag add angular-cli-ghpages@X.X.X-rc.X next ``` -cd angular-cli-ghpages/src -npm run build -npm run test -npm run publish-to-npm -npm dist-tag add angular-cli-ghpages@0.6.0-rc.0 next + +## Programmatic Usage + +For advanced use cases, `angular-cli-ghpages` can be used programmatically in Node.js scripts: + +```typescript +import { deployToGHPages, defaults, Schema } from 'angular-cli-ghpages'; + +// Deploy with custom options +const options: Schema = { + ...defaults, + dir: 'dist/my-app/browser', + repo: 'https://github.com/user/repo.git', + message: 'Custom deploy message', + branch: 'gh-pages', + name: 'Deploy Bot', + email: 'bot@example.com' +}; + +// Simple logger implementation +const logger = { + info: (msg: string) => console.log(msg), + warn: (msg: string) => console.warn(msg), + error: (msg: string) => console.error(msg), + debug: (msg: string) => console.debug(msg), + fatal: (msg: string) => console.error(msg) +}; + +try { + await deployToGHPages('dist/my-app/browser', options, logger); + console.log('Deployment successful!'); +} catch (error) { + console.error('Deployment failed:', error); +} ``` +### Available Types + +The package exports these TypeScript types for programmatic usage: + +- `Schema` - Complete options interface +- `PreparedOptions` - Internal options after processing +- `DeployUser` - User credentials type +- `GHPages` - gh-pages library wrapper interface +- `defaults` - Default configuration object + +### Advanced: Angular Builder Integration + +For custom Angular builders: + +```typescript +import { angularDeploy } from 'angular-cli-ghpages'; + +// Inside your custom builder +const result = await angularDeploy(context, builderConfig, 'your-project-name'); +``` + +**Note:** The CLI (`ng deploy`) remains the primary and recommended way to use this tool. Programmatic usage is considered advanced/experimental and may change between versions. + +## Dependency on gh-pages Internal API + +### Remote URL Discovery + +The `getRemoteUrl` function in `src/engine/engine.prepare-options-helpers.ts` calls into `gh-pages/lib/git`, which is an **internal API** not documented in gh-pages' public interface. This dependency carries upgrade risk. + +**What we depend on:** +- `new Git(process.cwd(), options.git).getRemoteUrl(options.remote)` from `gh-pages/lib/git` +- The exact error message format when remote doesn't exist or not in a git repository + +**Upgrade process for gh-pages v6+:** + +1. Check test failures in `src/engine/engine.prepare-options-helpers.spec.ts` first, specifically the `getRemoteUrl` test block +2. If those tests fail, it likely indicates a breaking change in gh-pages' internal Git API +3. Options: + - If `gh-pages/lib/git` still exists with same interface: update our error message assertions + - If the internal API changed significantly: implement our own git remote discovery using `child_process.execSync('git config --get remote.{remote}.url')` + - If gh-pages added a public API for this: switch to the public API + +**Current baseline:** Tests are pinned to gh-pages v6.3.0 behavior and error messages. + ## Keeping track of all the forks [ngx-deploy-starter](https://github.com/angular-schule/ngx-deploy-starter/) and diff --git a/docs/README_environment_github_actions.md b/docs/README_environment_github_actions.md index 61fa9e7..04be7e2 100644 --- a/docs/README_environment_github_actions.md +++ b/docs/README_environment_github_actions.md @@ -1,10 +1,10 @@ # angular-cli-ghpages: README for setting up deployment with GitHub Actions -As Github has introduced [Github Actions](https://github.com/features/actions), I would prefer to run all my CI tasks in it only, rather than going to some other CI providers. This guide is aimed to help out developers, who want to deploy their Angular app in Github Page using [angular-cli-pages](https://github.com/angular-schule/angular-cli-ghpages). +As GitHub has introduced [GitHub Actions](https://github.com/features/actions), I would prefer to run all my CI tasks in it only, rather than going to some other CI providers. This guide is aimed to help out developers, who want to deploy their Angular app in GitHub Pages using [angular-cli-pages](https://github.com/angular-schule/angular-cli-ghpages). ## Prerequisites -1. You've signed up for the GitHub Actions beta and taken a look at the documentation for the new [GitHub Actions](https://github.com/features/actions) format and/or understand the purpose of GitHub Actions. +1. You've taken a look at the documentation for [GitHub Actions](https://github.com/features/actions) and/or understand the purpose of GitHub Actions. 2. You've written some form of YML code before and/or have minimal knowledge of the syntax 3. You already have a working Angular app that can be deployed to GitHub Pages 4. You have already added **angular-cli-ghpages** in your project. If not: @@ -25,9 +25,9 @@ You can use the `GITHUB_TOKEN` to authenticate in a workflow run. More info can be found here: [help.github.com](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#using-the-github_token-in-a-workflow) -### Setup Github Action Flow +### Setup GitHub Action Flow -1. Open up your Github repo. +1. Open up your GitHub repo. 2. Go to **Settings** . Then on the side bar, go to **Actions** then **General**. @@ -76,23 +76,23 @@ More info can be found here: - It can be helpful to not have your workflows run on every push to every branch in the repo. - - For example, you can have your workflow run on push events to master and release branches: + - For example, you can have your workflow run on push events to main and release branches: ```yml on: push: branches: - - master + - main - release/* ``` - - or only run on pull_request events that target the master branch: + - or only run on pull_request events that target the main branch: ```yml on: pull_request: branches: - - master + - main ``` - or, run every day of the week from Monday - Friday at 02:00: @@ -114,7 +114,7 @@ More info can be found here: Learn everything you need to know in the following extended article. -[![Banner](https://angular-schule.github.io/website-articles/blog/2020-01-everything-github/everything-github.png)](https://angular.schule/blog/2020-01-everything-github) +[![Banner](https://angular-schule.github.io/website-articles/2020-01-everything-github/everything-github.png)](https://angular.schule/blog/2020-01-everything-github) **Everything GitHub: Continuous Integration, Deployment and Hosting for your Angular App** diff --git a/docs/README_standalone.md b/docs/README_standalone.md index 1bb452d..25f3ece 100644 --- a/docs/README_standalone.md +++ b/docs/README_standalone.md @@ -51,11 +51,47 @@ ionic build --prod -- --base-href=https://USERNAME.github.io/REPOSITORY_NAME/` npx angular-cli-ghpages --dir=www ``` -## Extra +## Build Target Parameters (Angular Builder Only) + +The following parameters are **only available** when using `ng deploy` (Angular Builder integration) and are **not supported** by the standalone CLI: + +- `--build-target` / `buildTarget` +- `--prerender-target` / `prerenderTarget` +- `--no-build` / `noBuild` +- `--base-href` / `baseHref` (only as a build option; use `ng build --base-href` instead) + +These parameters control Angular CLI's build process integration and automatic build triggering. + +**When using the standalone CLI** (`npx angular-cli-ghpages`): +- You must **build your project separately** before deployment +- Use `--dir` to specify the pre-built output directory +- The standalone CLI will **not** trigger any build process + +**Example standalone workflow:** + +```bash +# 1. Build your project first +ng build --configuration=production --base-href=/my-app/ + +# 2. Then deploy the built output +npx angular-cli-ghpages --dir=dist/my-app/browser +``` + +**For SSG/Prerendering with standalone CLI:** + +```bash +# 1. Prerender your project first +ng run my-app:prerender:production + +# 2. Then deploy the prerendered output +npx angular-cli-ghpages --dir=dist/my-app/browser +``` + +If you need automatic build integration, use `ng deploy` instead of the standalone CLI. See the [main README](../README.md) for Angular Builder usage. -For your convenience, the command will recognize the [environment variable](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings) `GH_TOKEN` and will replace this pattern in the `--repo` string. +## Extra -In example, the following command runs [on our Travis-CI](https://travis-ci.org/angular-buch/book-monkey2): +For your convenience, the command will recognize the environment variable `GH_TOKEN` and will replace this pattern in the `--repo` string. ```bash npx angular-cli-ghpages --repo=https://GH_TOKEN@github.com//.git --name="Displayed Username" --email=mail@example.org @@ -169,7 +205,7 @@ Run through without making any changes. This can be very usefull, because it out - Example: - `npx angular-cli-ghpages --cname=example.com` -A CNAME file will be created enabling you to use a custom domain. [More information on Github Pages using a custom domain](https://help.github.com/articles/using-a-custom-domain-with-github-pages/). +A CNAME file will be created enabling you to use a custom domain. [More information on GitHub Pages using a custom domain](https://help.github.com/articles/using-a-custom-domain-with-github-pages/). #### --add @@ -182,6 +218,31 @@ If is set to `true`, it will only add, and never remove existing files. By default, existing files in the target branch are removed before adding the ones. [More information](https://www.npmjs.com/package/gh-pages#optionsadd). +#### --no-notfound + +- **optional** +- Default: `notfound` is `true` (a `404.html` file is created) +- Example: + - `npx angular-cli-ghpages` -- A `404.html` file is created (copy of `index.html`). + - `npx angular-cli-ghpages --no-notfound` -- No `404.html` file is created. + +By default, a `404.html` file is created as a copy of `index.html`. +This is required for GitHub Pages to support SPA routing (though it returns HTTP 404 status). + +**Important for Cloudflare Pages:** Use `--no-notfound` when deploying to Cloudflare Pages. +The presence of a `404.html` file disables Cloudflare's native SPA mode. + +#### --no-nojekyll + +- **optional** +- Default: `nojekyll` is `true` (a `.nojekyll` file is created) +- Example: + - `npx angular-cli-ghpages` -- A `.nojekyll` file is created. + - `npx angular-cli-ghpages --no-nojekyll` -- No `.nojekyll` file is created. + +By default, a `.nojekyll` file is created to bypass Jekyll processing on GitHub Pages. +This prevents GitHub from ignoring files that start with an underscore (`_`). + ## FAQ Before posting any issue, [please read the FAQ first](https://github.com/angular-schule/angular-cli-ghpages/wiki/FAQ). diff --git a/src/.npmrc b/src/.npmrc new file mode 100644 index 0000000..e60ae71 --- /dev/null +++ b/src/.npmrc @@ -0,0 +1 @@ +ignore-scripts=false diff --git a/src/angular-cli-ghpages b/src/angular-cli-ghpages index 76de2b7..b1b8840 100755 --- a/src/angular-cli-ghpages +++ b/src/angular-cli-ghpages @@ -6,11 +6,10 @@ var path = require('path'), engine = require('./engine/engine'), defaults = require('./engine/defaults').defaults, pjson = require('./package.json'), - commander = require('commander'); + commander = require('./commander-fork'); -// defaults-file was ignored for --no-dotfiles, so it is not used here -// TODO: review https://github.com/tj/commander.js/issues/928 and check again if it now works -// (this is not a big problem, because true is currently the desired value and this is also the value in the defaults file) +// Boolean defaults for --no-X options are handled via defaults.ts +// Commander v3 auto-sets dotfiles/notfound/nojekyll to true, which matches our defaults commander .version(pjson.version) .description(pjson.description) @@ -51,7 +50,7 @@ commander ) .option( '-S, --no-silent', - 'Deprecated! This paremeter is no longer needed. It will be ignored.' + 'Deprecated! This parameter is no longer needed. It will be ignored.' ) .option( '-T, --no-dotfiles', diff --git a/src/angular-cli-ghpages.spec.ts b/src/angular-cli-ghpages.spec.ts deleted file mode 100755 index 6349bf8..0000000 --- a/src/angular-cli-ghpages.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import { execSync } from 'child_process'; - - -function runCliWithArgs(args) { - - const distFolder = path.resolve(__dirname, 'dist'); - - if (!fs.existsSync(distFolder)) { - throw new Error(`Dist directory ${distFolder} not found. Can't execute test! The directory must exist from the last build.`); - } - const program = path.resolve(__dirname, 'dist/angular-cli-ghpages'); - return execSync(`node ${program} --dry-run ${args}`).toString(); -} - -describe('Commander CLI options', () => { - - test('should set dotfiles, notfound, and nojekyll to `true` by default', () => { - const output = runCliWithArgs(''); - expect(output).toContain(`"dotfiles": "files starting with dot ('.') will be included"`); - expect(output).toContain('"notfound": "a 404.html file will be created"'); - expect(output).toContain('"nojekyll": "a .nojekyll file will be created"'); - }); - - test('should set dotfiles, notfound, and nojekyll to `false` with no- flags', () => { - const output = runCliWithArgs('--no-dotfiles --no-notfound --no-nojekyll'); - expect(output).toMatch(`"dotfiles": "files starting with dot ('.') will be ignored"`); - expect(output).toMatch('"notfound": "a 404.html file will NOT be created"'); - expect(output).toMatch('"nojekyll": "a .nojekyll file will NOT be created"'); - }); -}); diff --git a/src/commander-fork/LICENSE b/src/commander-fork/LICENSE new file mode 100644 index 0000000..10f997a --- /dev/null +++ b/src/commander-fork/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/commander-fork/README.md b/src/commander-fork/README.md new file mode 100644 index 0000000..2558c3d --- /dev/null +++ b/src/commander-fork/README.md @@ -0,0 +1,114 @@ +# Commander Fork + +**Minimal subset fork of commander v3.0.2 for angular-cli-ghpages.** + +## 🚨 INTERNAL USE ONLY + +**This fork exists SOLELY for angular-cli-ghpages and will NEVER be a general-purpose library.** + +- ❌ NOT for public adoption or use by other projects +- ❌ NOT a community fork +- ❌ NO migration support or guidance +- ❌ NO feature requests accepted +- βœ… ONLY maintained for angular-cli-ghpages internal needs + +**If you need commander.js, use the official upstream library:** https://github.com/tj/commander.js + +--- + +⚠️ **This is NOT a drop-in replacement for commander v3.0.2** + +This fork intentionally removes features (subcommands, actions, etc.) and preserves only the option parsing functionality needed by angular-cli-ghpages. + +## What This Fork Contains + +### Features Kept βœ… + +- `.version(str, flags?, description?)` - Set version and auto-register `-V, --version` +- `.description(str)` - Set command description +- `.option(flags, description, fn?, defaultValue?)` - Define options with `--no-` prefix support +- `.parse(argv)` - Parse command-line arguments +- `.opts()` - Get parsed options as object +- Property access (e.g., `program.dir`, `program.repo`) +- Basic `--help` output +- Error handling for unknown options and missing arguments + +### Features Removed ❌ + +- Subcommands (`.command()` with action handlers) +- Git-style executable subcommands +- Action handlers (`.action()`) +- Argument definitions (`.arguments()`) +- Advanced help customization +- Custom event listeners (except version and help) +- Most EventEmitter functionality + +## Compatibility Matrix + +| Capability | Commander v3.0.2 | commander-fork | +| --------------------------- | ---------------: | -------------: | +| Options parsing | βœ… | βœ… | +| Negated booleans (`--no-`) | βœ… | βœ… | +| Version flag (`-V`) | βœ… | βœ… | +| Help flag (`-h, --help`) | βœ… | βœ… | +| Option coercion functions | βœ… | βœ… | +| Regex validation | βœ… | βœ… | +| `.opts()` method | βœ… | βœ… | +| Subcommands (`.command()`) | βœ… | ❌ | +| `.action()` handlers | βœ… | ❌ | +| `.arguments()` definition | βœ… | ❌ | +| Custom EventEmitter events | βœ… | ❌ | + +## Supported API Surface + +### Methods You Can Use + +- βœ… `program.version(str, flags?, description?)` - Set version +- βœ… `program.description(str)` - Set description +- βœ… `program.option(flags, description, fn?, defaultValue?)` - Define option +- βœ… `program.parse(argv)` - Parse arguments +- βœ… `program.opts()` - Get options object +- βœ… `program.name(str?)` - Get/set name +- βœ… `program.usage(str?)` - Get/set usage +- βœ… `program.help()` - Output help and exit +- βœ… `program.helpInformation()` - Get help text +- βœ… `program.helpOption(flags?, description?)` - Customize help +- βœ… `program.allowUnknownOption()` - Allow unknown options +- βœ… Property access: `program.foo`, `program.bar` + +### Methods NOT Supported + +- ❌ `program.command(name, description?)` - Not implemented +- ❌ `program.action(fn)` - Not implemented +- ❌ `program.arguments(desc)` - Not implemented +- ❌ Custom EventEmitter listeners beyond version/help - Not implemented +- ❌ `prog help` subcommand pattern - Not implemented (use `-h` or `--help` flags instead) + +## Maintenance + +**This fork is maintained EXCLUSIVELY for angular-cli-ghpages.** + +This fork is **FROZEN** - we will NOT update it unless: +1. A critical security vulnerability is found that affects angular-cli-ghpages +2. A bug is discovered that affects angular-cli-ghpages functionality +3. Node.js changes break angular-cli-ghpages compatibility + +**We do NOT accept:** +- Feature requests from other projects +- Pull requests for general commander features +- Issues from external users + +For internal fixes (angular-cli-ghpages team only): +1. Update `commander-fork/index.js` +2. Add test in `commander-fork/test/` +3. Bump version to `3.0.2-fork.2`, etc. + +## Original Project + +This is a stripped-down fork of: +- **Repository**: https://github.com/tj/commander.js +- **Version**: v3.0.2 +- **License**: MIT +- **Original Author**: TJ Holowaychuk + +We are grateful for the original commander.js project and this fork exists only to preserve specific v3 behavior for our use case. diff --git a/src/commander-fork/index.d.ts b/src/commander-fork/index.d.ts new file mode 100644 index 0000000..58052b7 --- /dev/null +++ b/src/commander-fork/index.d.ts @@ -0,0 +1,150 @@ +// Type definitions for commander-fork (based on commander v3.0.2) +// Stripped to minimal API for angular-cli-ghpages +// Original: https://github.com/DefinitelyTyped/DefinitelyTyped + +/// + +declare namespace local { + + class Option { + flags: string; + required: boolean; + optional: boolean; + negate: boolean; + short?: string; + long: string; + description: string; + + constructor(flags: string, description?: string); + } + + class Command extends NodeJS.EventEmitter { + [key: string]: any; + + args: string[]; + + constructor(name?: string); + + /** + * Set the program version to `str`. + * + * This method auto-registers the "-V, --version" flag + * which will print the version number when passed. + * + * You can optionally supply the flags and description to override the defaults. + */ + version(str: string, flags?: string, description?: string): Command; + + /** + * Define option with `flags`, `description` and optional + * coercion `fn`. + * + * The `flags` string should contain both the short and long flags, + * separated by comma, a pipe or space. + * + * Examples: + * // simple boolean defaulting to false + * program.option('-p, --pepper', 'add pepper'); + * + * // simple boolean defaulting to true + * program.option('-C, --no-cheese', 'remove cheese'); + * + * // required argument + * program.option('-C, --chdir ', 'change the working directory'); + * + * // optional argument + * program.option('-c, --cheese [type]', 'add cheese [marble]'); + */ + option(flags: string, description?: string, fn?: ((arg1: any, arg2: any) => void) | RegExp, defaultValue?: any): Command; + option(flags: string, description?: string, defaultValue?: any): Command; + + /** + * Allow unknown options on the command line. + */ + allowUnknownOption(arg?: boolean): Command; + + /** + * Parse `argv`, settings options and invoking commands when defined. + */ + parse(argv: string[]): Command; + + /** + * Parse options from `argv` returning `argv` void of these options. + */ + parseOptions(argv: string[]): commander.ParseOptionsResult; + + /** + * Return an object containing options as key-value pairs + */ + opts(): { [key: string]: any }; + + /** + * Set the description to `str`. + */ + description(str: string, argsDescription?: {[argName: string]: string}): Command; + description(): string; + + /** + * Set or get the command usage. + */ + usage(str: string): Command; + usage(): string; + + /** + * Set the name of the command. + */ + name(str: string): Command; + + /** + * Get the name of the command. + */ + name(): string; + + /** + * Return help information for this command as a string. + */ + helpInformation(): string; + + /** + * Output help information for this command. + * + * When listener(s) are available for the helpLongFlag + * those callbacks are invoked. + */ + outputHelp(cb?: (str: string) => string): void; + + /** + * You can pass in flags and a description to override the help + * flags and help description for your command. + */ + helpOption(flags?: string, description?: string): Command; + + /** + * Output help information and exit. + */ + help(cb?: (str: string) => string): never; + } + +} + +declare namespace commander { + + type Command = local.Command + + type Option = local.Option + + interface ParseOptionsResult { + args: string[]; + unknown: string[]; + } + + interface CommanderStatic extends Command { + Command: typeof local.Command; + Option: typeof local.Option; + ParseOptionsResult: ParseOptionsResult; + } + +} + +declare const commander: commander.CommanderStatic; +export = commander; diff --git a/src/commander-fork/index.js b/src/commander-fork/index.js new file mode 100644 index 0000000..a83e232 --- /dev/null +++ b/src/commander-fork/index.js @@ -0,0 +1,754 @@ +/** + * commander-fork + * + * Minimal fork of commander v3.0.2 for angular-cli-ghpages + * + * Original: https://github.com/tj/commander.js (MIT License) + * Copyright (c) 2011 TJ Holowaychuk + * + * This fork strips out features we don't use: + * - Subcommands (.command() with action handlers) + * - Git-style executable subcommands + * - Action handlers (.action()) + * - Argument definitions (.arguments()) + * - Most EventEmitter functionality + * + * Features kept: + * - .version() - version flag registration + * - .description() - command description + * - .option() - option parsing with --no- prefix support + * - .parse() - argv parsing + * - .opts() - get parsed options + * - Property access (e.g., program.dir) + * - Basic --help output + */ + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter; +var path = require('path'); +var basename = path.basename; + +/** + * Inherit `Command` from `EventEmitter.prototype`. + */ + +require('util').inherits(Command, EventEmitter); + +/** + * Expose the root command. + */ + +exports = module.exports = new Command(); + +/** + * Expose `Command`. + */ + +exports.Command = Command; + +/** + * Expose `Option`. + */ + +exports.Option = Option; + +/** + * Initialize a new `Option` with the given `flags` and `description`. + * + * @param {String} flags + * @param {String} description + * @api public + */ + +function Option(flags, description) { + this.flags = flags; + this.required = flags.indexOf('<') >= 0; + this.optional = flags.indexOf('[') >= 0; + // FORK FIX 1/2: Tightened negate detection to avoid false positives + // Original: this.negate = flags.indexOf('-no-') !== -1; + // Problem: Would match '-no-' anywhere, e.g., '--enable-notifications' would incorrectly match + // Fix: Only match '--no-' at word boundaries (start of string or after delimiter) + this.negate = /(^|[\s,|])--no-/.test(flags); + flags = flags.split(/[ ,|]+/); + if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift(); + this.long = flags.shift(); + this.description = description || ''; +} + +/** + * Return option name. + * + * @return {String} + * @api private + */ + +Option.prototype.name = function() { + return this.long.replace(/^--/, ''); +}; + +/** + * Return option name, in a camelcase format that can be used + * as a object attribute key. + * + * @return {String} + * @api private + */ + +Option.prototype.attributeName = function() { + return camelcase(this.name().replace(/^no-/, '')); +}; + +/** + * Check if `arg` matches the short or long flag. + * + * @param {String} arg + * @return {Boolean} + * @api private + */ + +Option.prototype.is = function(arg) { + return this.short === arg || this.long === arg; +}; + +/** + * Initialize a new `Command`. + * + * @param {String} name + * @api public + */ + +function Command(name) { + this.options = []; + this._allowUnknownOption = false; + this._name = name || ''; + + this._helpFlags = '-h, --help'; + this._helpDescription = 'output usage information'; + this._helpShortFlag = '-h'; + this._helpLongFlag = '--help'; +} + +/** + * Define option with `flags`, `description` and optional + * coercion `fn`. + * + * The `flags` string should contain both the short and long flags, + * separated by comma, a pipe or space. The following are all valid + * all will output this way when `--help` is used. + * + * "-p, --pepper" + * "-p|--pepper" + * "-p --pepper" + * + * Examples: + * + * // simple boolean defaulting to undefined + * program.option('-p, --pepper', 'add pepper'); + * + * program.pepper + * // => undefined + * + * --pepper + * program.pepper + * // => true + * + * // simple boolean defaulting to true (unless non-negated option is also defined) + * program.option('-C, --no-cheese', 'remove cheese'); + * + * program.cheese + * // => true + * + * --no-cheese + * program.cheese + * // => false + * + * // required argument + * program.option('-C, --chdir ', 'change the working directory'); + * + * --chdir /tmp + * program.chdir + * // => "/tmp" + * + * // optional argument + * program.option('-c, --cheese [type]', 'add cheese [marble]'); + * + * @param {String} flags + * @param {String} description + * @param {Function|*} [fn] or default + * @param {*} [defaultValue] + * @return {Command} for chaining + * @api public + */ + +Command.prototype.option = function(flags, description, fn, defaultValue) { + var self = this, + option = new Option(flags, description), + oname = option.name(), + name = option.attributeName(); + + // default as 3rd arg + if (typeof fn !== 'function') { + if (fn instanceof RegExp) { + // This is a bit simplistic (especially no error messages), and probably better handled by caller using custom option processing. + // No longer documented in README, but still present for backwards compatibility. + var regex = fn; + fn = function(val, def) { + var m = regex.exec(val); + return m ? m[0] : def; + }; + } else { + defaultValue = fn; + fn = null; + } + } + + // preassign default value for --no-*, [optional], , or plain flag if boolean value + if (option.negate || option.optional || option.required || typeof defaultValue === 'boolean') { + // when --no-foo we make sure default is true, unless a --foo option is already defined + if (option.negate) { + var opts = self.opts(); + defaultValue = Object.prototype.hasOwnProperty.call(opts, name) ? opts[name] : true; + } + // preassign only if we have a default + if (defaultValue !== undefined) { + self[name] = defaultValue; + option.defaultValue = defaultValue; + } + } + + // register the option + this.options.push(option); + + // when it's passed assign the value + // and conditionally invoke the callback + this.on('option:' + oname, function(val) { + // coercion + if (val !== null && fn) { + val = fn(val, self[name] === undefined ? defaultValue : self[name]); + } + + // unassigned or boolean value + if (typeof self[name] === 'boolean' || typeof self[name] === 'undefined') { + // if no value, negate false, and we have a default, then use it! + if (val == null) { + self[name] = option.negate + ? false + : defaultValue || true; + } else { + self[name] = val; + } + } else if (val !== null) { + // reassign + self[name] = option.negate ? false : val; + } + }); + + return this; +}; + +/** + * Allow unknown options on the command line. + * + * @param {Boolean} arg if `true` or omitted, no error will be thrown + * for unknown options. + * @api public + */ +Command.prototype.allowUnknownOption = function(arg) { + this._allowUnknownOption = arguments.length === 0 || arg; + return this; +}; + +/** + * Parse `argv`, settings options and invoking commands when defined. + * + * @param {Array} argv + * @return {Command} for chaining + * @api public + */ + +Command.prototype.parse = function(argv) { + // store raw args + this.rawArgs = argv; + + // guess name + this._name = this._name || basename(argv[1], '.js'); + + // process argv + var normalized = this.normalize(argv.slice(2)); + var parsed = this.parseOptions(normalized); + this.args = parsed.args; + + // check for help + outputHelpIfNecessary(this, parsed.unknown); + + // unknown options + if (parsed.unknown.length > 0) { + this.unknownOption(parsed.unknown[0]); + } + + return this; +}; + +/** + * Normalize `args`, splitting joined short flags. For example + * the arg "-abc" is equivalent to "-a -b -c". + * This also normalizes equal sign and splits "--abc=def" into "--abc def". + * + * @param {Array} args + * @return {Array} + * @api private + */ + +Command.prototype.normalize = function(args) { + var ret = [], + arg, + lastOpt, + index, + short, + opt; + + for (var i = 0, len = args.length; i < len; ++i) { + arg = args[i]; + if (i > 0) { + lastOpt = this.optionFor(args[i - 1]); + } + + if (arg === '--') { + // Honor option terminator + ret = ret.concat(args.slice(i)); + break; + } else if (lastOpt && lastOpt.required) { + ret.push(arg); + } else if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { + short = arg.slice(0, 2); + opt = this.optionFor(short); + if (opt && (opt.required || opt.optional)) { + ret.push(short); + ret.push(arg.slice(2)); + } else { + arg.slice(1).split('').forEach(function(c) { + ret.push('-' + c); + }); + } + } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) { + ret.push(arg.slice(0, index), arg.slice(index + 1)); + } else { + ret.push(arg); + } + } + + return ret; +}; + +/** + * Return an option matching `arg` if any. + * + * @param {String} arg + * @return {Option} + * @api private + */ + +Command.prototype.optionFor = function(arg) { + for (var i = 0, len = this.options.length; i < len; ++i) { + if (this.options[i].is(arg)) { + return this.options[i]; + } + } +}; + +/** + * Parse options from `argv` returning `argv` + * void of these options. + * + * @param {Array} argv + * @return {Array} + * @api public + */ + +Command.prototype.parseOptions = function(argv) { + var args = [], + len = argv.length, + literal, + option, + arg; + + var unknownOptions = []; + + // parse options + for (var i = 0; i < len; ++i) { + arg = argv[i]; + + // literal args after -- + if (literal) { + args.push(arg); + continue; + } + + if (arg === '--') { + literal = true; + continue; + } + + // find matching Option + option = this.optionFor(arg); + + // option is defined + if (option) { + // requires arg + if (option.required) { + arg = argv[++i]; + if (arg == null) return this.optionMissingArgument(option); + this.emit('option:' + option.name(), arg); + // optional arg + } else if (option.optional) { + arg = argv[i + 1]; + if (arg == null || (arg[0] === '-' && arg !== '-')) { + arg = null; + } else { + ++i; + } + this.emit('option:' + option.name(), arg); + // flag + } else { + this.emit('option:' + option.name()); + } + continue; + } + + // looks like an option + if (arg.length > 1 && arg[0] === '-') { + unknownOptions.push(arg); + + // If the next argument looks like it might be + // an argument for this option, we pass it on. + // If it isn't, then it'll simply be ignored + if ((i + 1) < argv.length && (argv[i + 1][0] !== '-' || argv[i + 1] === '-')) { + unknownOptions.push(argv[++i]); + } + continue; + } + + // arg + args.push(arg); + } + + return { args: args, unknown: unknownOptions }; +}; + +/** + * Return an object containing options as key-value pairs + * + * @return {Object} + * @api public + */ +Command.prototype.opts = function() { + var result = {}, + len = this.options.length; + + for (var i = 0; i < len; i++) { + var key = this.options[i].attributeName(); + result[key] = key === this._versionOptionName ? this._version : this[key]; + } + return result; +}; + +/** + * `Option` is missing an argument, but received `flag` or nothing. + * + * @param {String} option + * @param {String} flag + * @api private + */ + +Command.prototype.optionMissingArgument = function(option, flag) { + if (flag) { + console.error("error: option '%s' argument missing, got '%s'", option.flags, flag); + } else { + console.error("error: option '%s' argument missing", option.flags); + } + process.exit(1); +}; + +/** + * Unknown option `flag`. + * + * @param {String} flag + * @api private + */ + +Command.prototype.unknownOption = function(flag) { + if (this._allowUnknownOption) return; + console.error("error: unknown option '%s'", flag); + process.exit(1); +}; + +/** + * Set the program version to `str`. + * + * This method auto-registers the "-V, --version" flag + * which will print the version number when passed. + * + * You can optionally supply the flags and description to override the defaults. + * + * @param {String} str + * @param {String} [flags] + * @param {String} [description] + * @return {Command} for chaining + * @api public + */ + +Command.prototype.version = function(str, flags, description) { + if (arguments.length === 0) return this._version; + this._version = str; + flags = flags || '-V, --version'; + description = description || 'output the version number'; + var versionOption = new Option(flags, description); + // FORK FIX 2/2: Support short-only and custom version flags properly + // Original: this._versionOptionName = versionOption.long.substr(2) || 'version'; + // Problem: .substr(2) on '-v' gives empty string, falls back to 'version', + // but event is 'option:v' (or 'option:-v'), so listener never fires. + // Also doesn't handle camelCase for flags like '--version-info'. + // Fix: Use attributeName() for proper camelCase conversion and short flag support. + // Examples: '-v' -> 'v', '--version' -> 'version', '--version-info' -> 'versionInfo' + this._versionOptionName = versionOption.attributeName(); + this.options.push(versionOption); + this.on('option:' + versionOption.name(), function() { + process.stdout.write(str + '\n'); + process.exit(0); + }); + return this; +}; + +/** + * Set the description to `str`. + * + * @param {String} str + * @param {Object} argsDescription + * @return {String|Command} + * @api public + */ + +Command.prototype.description = function(str, argsDescription) { + if (arguments.length === 0) return this._description; + this._description = str; + this._argsDescription = argsDescription; + return this; +}; + +/** + * Set / get the command usage `str`. + * + * @param {String} str + * @return {String|Command} + * @api public + */ + +Command.prototype.usage = function(str) { + var usage = '[options]'; + if (arguments.length === 0) return this._usage || usage; + this._usage = str; + return this; +}; + +/** + * Get or set the name of the command + * + * @param {String} str + * @return {String|Command} + * @api public + */ + +Command.prototype.name = function(str) { + if (arguments.length === 0) return this._name; + this._name = str; + return this; +}; + +/** + * Return the largest option length. + * + * @return {Number} + * @api private + */ + +Command.prototype.largestOptionLength = function() { + var options = [].slice.call(this.options); + options.push({ + flags: this._helpFlags + }); + + return options.reduce(function(max, option) { + return Math.max(max, option.flags.length); + }, 0); +}; + +/** + * Return the pad width. + * + * @return {Number} + * @api private + */ + +Command.prototype.padWidth = function() { + return this.largestOptionLength(); +}; + +/** + * Return help for options. + * + * @return {String} + * @api private + */ + +Command.prototype.optionHelp = function() { + var width = this.padWidth(); + + // Append the help information + return this.options.map(function(option) { + return pad(option.flags, width) + ' ' + option.description + + ((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : ''); + }).concat([pad(this._helpFlags, width) + ' ' + this._helpDescription]) + .join('\n'); +}; + +/** + * Return program help documentation. + * + * @return {String} + * @api private + */ + +Command.prototype.helpInformation = function() { + var desc = []; + if (this._description) { + desc = [ + this._description, + '' + ]; + } + + var cmdName = this._name; + var usage = [ + 'Usage: ' + cmdName + ' ' + this.usage(), + '' + ]; + + var options = [ + 'Options:', + '' + this.optionHelp().replace(/^/gm, ' '), + '' + ]; + + return usage + .concat(desc) + .concat(options) + .join('\n'); +}; + +/** + * Output help information for this command. + * + * When listener(s) are available for the helpLongFlag + * those callbacks are invoked. + * + * @api public + */ + +Command.prototype.outputHelp = function(cb) { + if (!cb) { + cb = function(passthru) { + return passthru; + }; + } + const cbOutput = cb(this.helpInformation()); + if (typeof cbOutput !== 'string' && !Buffer.isBuffer(cbOutput)) { + throw new Error('outputHelp callback must return a string or a Buffer'); + } + process.stdout.write(cbOutput); + this.emit(this._helpLongFlag); +}; + +/** + * You can pass in flags and a description to override the help + * flags and help description for your command. + * + * @param {String} [flags] + * @param {String} [description] + * @return {Command} + * @api public + */ + +Command.prototype.helpOption = function(flags, description) { + this._helpFlags = flags || this._helpFlags; + this._helpDescription = description || this._helpDescription; + + var splitFlags = this._helpFlags.split(/[ ,|]+/); + + if (splitFlags.length > 1) this._helpShortFlag = splitFlags.shift(); + + this._helpLongFlag = splitFlags.shift(); + + return this; +}; + +/** + * Output help information and exit. + * + * @param {Function} [cb] + * @api public + */ + +Command.prototype.help = function(cb) { + this.outputHelp(cb); + process.exit(); +}; + +/** + * Camel-case the given `flag` + * + * @param {String} flag + * @return {String} + * @api private + */ + +function camelcase(flag) { + return flag.split('-').reduce(function(str, word) { + return str + word[0].toUpperCase() + word.slice(1); + }); +} + +/** + * Pad `str` to `width`. + * + * @param {String} str + * @param {Number} width + * @return {String} + * @api private + */ + +function pad(str, width) { + var len = Math.max(0, width - str.length); + return str + Array(len + 1).join(' '); +} + +/** + * Output help information if necessary + * + * @param {Command} command to output help for + * @param {Array} array of options to search for -h or --help + * @api private + */ + +function outputHelpIfNecessary(cmd, options) { + options = options || []; + + for (var i = 0; i < options.length; i++) { + if (options[i] === cmd._helpLongFlag || options[i] === cmd._helpShortFlag) { + cmd.outputHelp(); + process.exit(0); + } + } +} diff --git a/src/commander-fork/test/test.command.allowUnknownOption.spec.ts b/src/commander-fork/test/test.command.allowUnknownOption.spec.ts new file mode 100644 index 0000000..fedd5a0 --- /dev/null +++ b/src/commander-fork/test/test.command.allowUnknownOption.spec.ts @@ -0,0 +1,77 @@ +/** + * Module dependencies. + */ +const commander = require('../'); +// var program = require('../') +// , sinon = require('sinon').sandbox.create() +// , should = require('should'); + +describe('command.allowUnknownOption', () => { +let stubError: jest.SpyInstance; +let stubExit: jest.SpyInstance; + +function resetStubStatus() { + stubError.mockClear(); + stubExit.mockClear(); +} + +beforeEach(() => { + stubError = jest.spyOn(console, 'error').mockImplementation(() => {}); + stubExit = jest.spyOn(process, 'exit').mockImplementation((code?: number) => { + return undefined as never; + }); +}); +afterEach(() => { + stubError.mockRestore(); + stubExit.mockRestore(); +}); + +it('should error on unknown option', () => { +let program = new commander.Command(); +program + .version('0.0.1') + .option('-p, --pepper', 'add pepper'); +program.parse('node test -m'.split(' ')); + +expect(stubError).toHaveBeenCalledTimes(1); +}); + +// // test subcommand +// resetStubStatus(); +// program +// .command('sub') +// .action(function () { +// }); +// program.parse('node test sub -m'.split(' ')); +// +// stubError.callCount.should.equal(1); +// stubExit.calledOnce.should.be.true(); + +it('should not error with allowUnknownOption', () => { +// command with `allowUnknownOption` +resetStubStatus(); +let program = new commander.Command(); +program + .version('0.0.1') + .option('-p, --pepper', 'add pepper'); +program + .allowUnknownOption() + .parse('node test -m'.split(' ')); + +expect(stubError).toHaveBeenCalledTimes(0); +expect(stubExit).not.toHaveBeenCalled(); +}); + +// // subcommand with `allowUnknownOption` +// resetStubStatus(); +// program +// .command('sub2') +// .allowUnknownOption() +// .action(function () { +// }); +// program.parse('node test sub2 -m'.split(' ')); +// +// stubError.callCount.should.equal(0); +// stubExit.calledOnce.should.be.false(); + +}); diff --git a/src/commander-fork/test/test.fork-fixes.spec.ts b/src/commander-fork/test/test.fork-fixes.spec.ts new file mode 100644 index 0000000..c188ac1 --- /dev/null +++ b/src/commander-fork/test/test.fork-fixes.spec.ts @@ -0,0 +1,236 @@ +/** + * Tests for angular-cli-ghpages fork-specific fixes and features + * + * This file contains tests for the 2 intentional improvements we made + * to commander v3.0.2 for angular-cli-ghpages: + * + * FIX 1/2: Tightened negate detection regex + * FIX 2/2: Support for short-only and custom version flags + * + * These tests do NOT exist in the original commander v3.0.2. + */ + +const commander = require('../'); + +describe('Fork Fix 1: Negate detection with tightened regex', () => { + // This is implicitly tested by test.options.bool.no.spec.ts + // No additional tests needed here +}); + +describe('Fork Fix 2: Version short-only and custom flags', () => { + describe('version() with short-only flag', () => { + let exitSpy: jest.SpyInstance; + let writeSpy: jest.SpyInstance; + let captured: {out: string, code: null | number}; + + beforeEach(() => { + captured = {out: '', code: null}; + exitSpy = jest.spyOn(process, 'exit').mockImplementation((c?: number) => { + captured.code = c ?? 0; + return undefined as never; + }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation((s: string | Uint8Array) => { + captured.out += s; + return true; + }); + }); + + afterEach(() => { + exitSpy.mockRestore(); + writeSpy.mockRestore(); + }); + + it('prints version and exits with -v short-only flag', () => { + const program = new commander.Command(); + program.version('1.2.3', '-v'); + program.parse(['node', 'x', '-v']); + expect(captured.out).toBe('1.2.3\n'); + expect(captured.code).toBe(0); + }); + + it('prints version with long-only custom flag', () => { + captured = {out: '', code: null}; + const program = new commander.Command(); + program.version('2.0.0', '--ver'); + program.parse(['node', 'x', '--ver']); + expect(captured.out).toBe('2.0.0\n'); + expect(captured.code).toBe(0); + }); + + it('prints version with combined custom flags', () => { + captured = {out: '', code: null}; + const program = new commander.Command(); + program.version('3.0.0', '-v, --ver'); + program.parse(['node', 'x', '--ver']); + expect(captured.out).toBe('3.0.0\n'); + expect(captured.code).toBe(0); + }); + }); + + describe('helpOption() with custom flags', () => { + let exitSpy: jest.SpyInstance; + let writeSpy: jest.SpyInstance; + let exitCode: null | number; + + beforeEach(() => { + exitCode = null; + exitSpy = jest.spyOn(process, 'exit').mockImplementation((c?: number) => { + exitCode = c ?? 0; + throw new Error(`exit(${c})`); + }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + }); + + afterEach(() => { + exitSpy.mockRestore(); + writeSpy.mockRestore(); + }); + + it('fires help listener with default flags', () => { + const program = new commander.Command(); + const spy = jest.fn(); + program.on('--help', spy); + expect(() => program.parse(['node', 'x', '--help'])).toThrow('exit(0)'); + expect(spy).toHaveBeenCalled(); + expect(exitCode).toBe(0); + }); + + it('fires help listener after helpOption override', () => { + const program = new commander.Command().helpOption('-?, --helpme'); + const spy = jest.fn(); + program.on('--helpme', spy); + expect(() => program.parse(['node', 'x', '--helpme'])).toThrow('exit(0)'); + expect(spy).toHaveBeenCalled(); + expect(exitCode).toBe(0); + }); + }); + + describe('opts() includes version with custom flags', () => { + let exitSpy: jest.SpyInstance; + let writeSpy: jest.SpyInstance; + + beforeEach(() => { + exitSpy = jest.spyOn(process, 'exit').mockImplementation((code?: number) => { + return undefined as never; + }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); + }); + + afterEach(() => { + exitSpy.mockRestore(); + writeSpy.mockRestore(); + }); + + it('includes version in opts() with default flags', () => { + const program = new commander.Command(); + program.version('1.2.3'); + program.parse(['node', 'x']); + + const opts = program.opts(); + expect(opts.version).toBe('1.2.3'); + }); + + it('includes version in opts() with custom long flag', () => { + const program = new commander.Command(); + program.version('2.0.0', '-V, --ver'); + program.parse(['node', 'x']); + + const opts = program.opts(); + expect(opts.ver).toBe('2.0.0'); + }); + + it('includes version in opts() with short-only flag', () => { + const program = new commander.Command(); + program.version('3.1.4', '-v'); + program.parse(['node', 'x']); + + const opts = program.opts(); + // Note: Short-only flags like '-v' become capital 'V' in opts due to camelCase conversion + expect(opts.V).toBe('3.1.4'); + }); + + it('includes version in opts() with long-only flag', () => { + const program = new commander.Command(); + program.version('1.0.0', '--version-info'); + program.parse(['node', 'x']); + + const opts = program.opts(); + expect(opts.versionInfo).toBe('1.0.0'); + }); + + it('includes version alongside other options', () => { + const program = new commander.Command(); + program + .version('1.2.3') + .option('-d, --dir ', 'directory', 'dist') + .option('-v, --verbose', 'verbose'); + program.parse(['node', 'x', '--verbose']); + + const opts = program.opts(); + expect(opts.version).toBe('1.2.3'); + expect(opts.dir).toBe('dist'); + expect(opts.verbose).toBe(true); + }); + }); + + describe('unknown options with attached values', () => { + let errSpy: jest.SpyInstance; + let exitSpy: jest.SpyInstance; + + beforeEach(() => { + errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + exitSpy = jest.spyOn(process, 'exit').mockImplementation((code?: number) => { + return undefined as never; + }); + }); + + afterEach(() => { + errSpy.mockRestore(); + exitSpy.mockRestore(); + }); + + describe('errors by default', () => { + test.each([ + ['--mystery=42'], + ['--mystery='], + ['--mystery', '-5'], + ])('errors on %p', (...args) => { + const p = new commander.Command().option('-k, --known '); + p.parse(['node', 'x', ...args]); + expect(errSpy).toHaveBeenCalled(); + }); + }); + + describe('allowed with allowUnknownOption', () => { + test.each([ + ['--mystery=42'], + ['--mystery='], + ['--mystery', '-5'], + ])('allowed with %p', (...args) => { + errSpy.mockClear(); + const p = new commander.Command().allowUnknownOption(); + p.parse(['node', 'x', ...args]); + expect(errSpy).not.toHaveBeenCalled(); + }); + }); + + it('handles mixed known and unknown options', () => { + errSpy.mockClear(); + const p = new commander.Command() + .option('-k, --known ') + .allowUnknownOption(); + p.parse(['node', 'x', '-k', 'knownValue', '--unknown=foo']); + expect(errSpy).not.toHaveBeenCalled(); + expect(p.known).toBe('knownValue'); + }); + + it('handles equals with spaces in value', () => { + errSpy.mockClear(); + const p = new commander.Command().allowUnknownOption(); + p.parse(['node', 'x', '--mystery=some value']); + expect(errSpy).not.toHaveBeenCalled(); + }); + }); +}); + +export {}; diff --git a/src/commander-fork/test/test.options.bool.no.spec.ts b/src/commander-fork/test/test.options.bool.no.spec.ts new file mode 100644 index 0000000..8d5da0b --- /dev/null +++ b/src/commander-fork/test/test.options.bool.no.spec.ts @@ -0,0 +1,78 @@ +const commander = require('../'); +// require('should'); + +describe('options.bool.no', () => { +// Test combination of flag and --no-flag +// (negatable flag on its own is tested in test.options.bool.js) + +function flagProgram(defaultValue?: boolean) { + const program = new commander.Command(); + program + .option('-p, --pepper', 'add pepper', defaultValue) + .option('-P, --no-pepper', 'remove pepper'); + return program; +} + +it('Flag with no default, normal usage - no options', () => { + +const programNoDefaultNoOptions = flagProgram(); +programNoDefaultNoOptions.parse(['node', 'test']); +expect(programNoDefaultNoOptions).not.toHaveProperty('pepper'); +}); +it('Flag with no default, normal usage - with flag', () => { +const programNoDefaultWithFlag = flagProgram(); +programNoDefaultWithFlag.parse(['node', 'test', '--pepper']); +expect(programNoDefaultWithFlag.pepper).toBe(true); +}); +it('Flag with no default, normal usage - with neg flag', () => { +const programNoDefaultWithNegFlag = flagProgram(); +programNoDefaultWithNegFlag.parse(['node', 'test', '--no-pepper']); +expect(programNoDefaultWithNegFlag.pepper).toBe(false); +}); +it('Flag with default true - no options', () => { +// Flag with default, say from an environment variable. + +const programTrueDefaultNoOptions = flagProgram(true); +programTrueDefaultNoOptions.parse(['node', 'test']); +expect(programTrueDefaultNoOptions.pepper).toBe(true); +}); +it('Flag with default true - with flag', () => { +const programTrueDefaultWithFlag = flagProgram(true); +programTrueDefaultWithFlag.parse(['node', 'test', '-p']); +expect(programTrueDefaultWithFlag.pepper).toBe(true); +}); +it('Flag with default true - with neg flag', () => { +const programTrueDefaultWithNegFlag = flagProgram(true); +programTrueDefaultWithNegFlag.parse(['node', 'test', '-P']); +expect(programTrueDefaultWithNegFlag.pepper).toBe(false); +}); +it('Flag with default false - no options', () => { +const programFalseDefaultNoOptions = flagProgram(false); +programFalseDefaultNoOptions.parse(['node', 'test']); +expect(programFalseDefaultNoOptions.pepper).toBe(false); +}); +it('Flag with default false - with flag', () => { +const programFalseDefaultWithFlag = flagProgram(false); +programFalseDefaultWithFlag.parse(['node', 'test', '-p']); +expect(programFalseDefaultWithFlag.pepper).toBe(true); +}); +it('Flag with default false - with neg flag', () => { +const programFalseDefaultWithNegFlag = flagProgram(false); +programFalseDefaultWithNegFlag.parse(['node', 'test', '-P']); +expect(programFalseDefaultWithNegFlag.pepper).toBe(false); +}); +it('Flag specified both ways, last one wins - no then yes', () => { +// Flag specified both ways, last one wins. + +const programNoYes = flagProgram(); +programNoYes.parse(['node', 'test', '--no-pepper', '--pepper']); +expect(programNoYes.pepper).toBe(true); +}); +it('Flag specified both ways, last one wins - yes then no', () => { +const programYesNo = flagProgram(); +programYesNo.parse(['node', 'test', '--pepper', '--no-pepper']); +expect(programYesNo.pepper).toBe(false); +}); +}); + +export {}; diff --git a/src/commander-fork/test/test.options.coercion.spec.ts b/src/commander-fork/test/test.options.coercion.spec.ts new file mode 100644 index 0000000..66c43ad --- /dev/null +++ b/src/commander-fork/test/test.options.coercion.spec.ts @@ -0,0 +1,43 @@ +/** + * Module dependencies. + */ +const commander = require('../'); +// var program = require('../') +// , should = require('should'); + +describe('options.coercion', () => { +function parseRange(str: string): number[] { + return str.split('..').map(Number); +} + +function increaseVerbosity(v: string, total: number): number { + return total + 1; +} + +function collectValues(str: string, memo: string[]): string[] { + memo.push(str); + return memo; +} + +it('should coerce values correctly', () => { +let program = new commander.Command(); +program + .version('0.0.1') + .option('-i, --int ', 'pass an int', parseInt) + .option('-n, --num ', 'pass a number', Number) + .option('-f, --float ', 'pass a float', parseFloat) + .option('-r, --range ', 'pass a range', parseRange) + .option('-v, --verbose', 'increase verbosity', increaseVerbosity, 0) + .option('-c, --collect ', 'add a string (can be used multiple times)', collectValues, []); + +program.parse('node test -i 5.5 -f 5.5 -n 15.99 -r 1..5 -c foo -c bar -c baz -vvvv --verbose'.split(' ')); +expect(program.int).toBe(5); +expect(program.num).toBe(15.99); +expect(program.float).toBe(5.5); +expect(program.range).toEqual([1, 5]); +expect(program.collect).toEqual(['foo', 'bar', 'baz']); +expect(program.verbose).toBe(5); +}); +}); + +export {}; diff --git a/src/commander-fork/test/test.options.regex.spec.ts b/src/commander-fork/test/test.options.regex.spec.ts new file mode 100644 index 0000000..4783a43 --- /dev/null +++ b/src/commander-fork/test/test.options.regex.spec.ts @@ -0,0 +1,20 @@ +/** + * Module dependencies. + */ +// var program = require('../') +// , should = require('should'); + +describe('options.regex', () => { +it('should validate with regex', () => { +const commanderRegex = require('../'); +let program = new commanderRegex.Command(); +program + .version('0.0.1') + .option('-s, --size ', 'Pizza Size', /^(large|medium|small)$/i, 'medium') + .option('-d, --drink [drink]', 'Drink', /^(Coke|Pepsi|Izze)$/i) + +program.parse('node test -s big -d coke'.split(' ')); +expect(program.size).toBe('medium'); +expect(program.drink).toBe('coke'); +}); +}); diff --git a/src/commander-fork/test/test.options.version.spec.ts b/src/commander-fork/test/test.options.version.spec.ts new file mode 100644 index 0000000..a4dbf60 --- /dev/null +++ b/src/commander-fork/test/test.options.version.spec.ts @@ -0,0 +1,40 @@ +const commander = require('../'); +// var program = require('../') +// , should = require('should'); + +describe('options.version', () => { +let capturedExitCode: number; +let capturedOutput: string; +let exitSpy: jest.SpyInstance; +let writeSpy: jest.SpyInstance; + +// program.version('0.0.1'); + +['-V', '--version'].forEach(function (flag) { + it(`should output version with ${flag}`, () => { + const program = new commander.Command(); + program.version('0.0.1'); + capturedExitCode = -1; + capturedOutput = ''; + + exitSpy = jest.spyOn(process, 'exit').mockImplementation((code?: number) => { + capturedExitCode = code ?? 0; + return undefined as never; + }); + writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation((output: string | Uint8Array) => { + capturedOutput += output; + return true; + }); + + program.parse(['node', 'test', flag]); + + exitSpy.mockRestore(); + writeSpy.mockRestore(); + + expect(capturedOutput).toBe('0.0.1\n'); + expect(capturedExitCode).toBe(0); + }); +}) +}); + +export {}; diff --git a/src/deploy/actions.spec.ts b/src/deploy/actions.spec.ts index 84e3cf3..2c33a5b 100644 --- a/src/deploy/actions.spec.ts +++ b/src/deploy/actions.spec.ts @@ -7,11 +7,16 @@ import { } from '@angular-devkit/architect/src'; import { JsonObject, logging } from '@angular-devkit/core'; import { BuildTarget } from '../interfaces'; +import { Schema } from './schema'; import deploy from './actions'; +interface EngineHost { + run(dir: string, options: Schema, logger: logging.LoggerApi): Promise; +} + let context: BuilderContext; -const mockEngine = { run: (_: string, __: any, __2: any) => Promise.resolve() }; +const mockEngine: EngineHost = { run: (_: string, __: Schema, __2: logging.LoggerApi) => Promise.resolve() }; const PROJECT = 'pirojok-project'; const BUILD_TARGET: BuildTarget = { @@ -59,12 +64,10 @@ describe('Deploy Angular apps', () => { describe('error handling', () => { it('throws if there is no target project', async () => { context.target = undefined; - try { - await deploy(mockEngine, context, BUILD_TARGET, {}); - fail(); - } catch (e) { - expect(e.message).toMatch(/Cannot execute the build target/); - } + + await expect( + deploy(mockEngine, context, BUILD_TARGET, {}) + ).rejects.toThrow('Cannot execute the build target'); }); it('throws if app building fails', async () => { @@ -76,18 +79,70 @@ describe('Deploy Angular apps', () => { Promise.resolve({ result: Promise.resolve(createBuilderOutputMock(false)) } as BuilderRun); - try { - await deploy(mockEngine, context, BUILD_TARGET, {}); - fail(); - } catch (e) { - expect(e.message).toEqual('Error while building the app.'); - } + + await expect( + deploy(mockEngine, context, BUILD_TARGET, {}) + ).rejects.toThrow('Error while building the app.'); + }); + + it('throws if outputPath has invalid shape', async () => { + context.getTargetOptions = (_: Target) => + Promise.resolve({ + outputPath: { browser: 'browser' } // missing required 'base' + } as JsonObject); + + await expect( + deploy(mockEngine, context, BUILD_TARGET, { noBuild: false }) + ).rejects.toThrow(/Unsupported outputPath configuration/); + }); + }); + + describe('outputPath resolution', () => { + const captureDir = async (outputPath: unknown): Promise => { + let capturedDir = ''; + const mockEngineWithCapture: EngineHost = { + run: (dir: string, _options: Schema, _logger: logging.LoggerApi) => { + capturedDir = dir; + return Promise.resolve(); + } + }; + + context.getTargetOptions = (_: Target) => + Promise.resolve({ outputPath } as JsonObject); + + await deploy(mockEngineWithCapture, context, BUILD_TARGET, { noBuild: false }); + return capturedDir; + }; + + it('uses default path when outputPath is undefined (Angular 20+)', async () => { + const dir = await captureDir(undefined); + expect(dir).toBe(`dist/${PROJECT}/browser`); + }); + + it('appends /browser when outputPath is string (Angular 18-19)', async () => { + const dir = await captureDir('dist/my-app'); + expect(dir).toBe('dist/my-app/browser'); + }); + + it('uses base/browser when outputPath is object', async () => { + const dir = await captureDir({ base: 'dist/my-app', browser: 'browser' }); + expect(dir).toBe('dist/my-app/browser'); + }); + + it('defaults browser to "browser" when omitted from object', async () => { + const dir = await captureDir({ base: 'dist/my-app' }); + expect(dir).toBe('dist/my-app/browser'); + }); + + it('uses base only when browser is empty string (SPA mode)', async () => { + const dir = await captureDir({ base: 'dist/my-app', browser: '' }); + expect(dir).toBe('dist/my-app'); }); }); }); const initMocks = () => { - context = { + const mockContext: Partial = { target: { configuration: 'production', project: PROJECT, @@ -103,13 +158,12 @@ const initMocks = () => { logger: new logging.NullLogger(), workspaceRoot: 'cwd', addTeardown: _ => {}, - validateOptions: _ => Promise.resolve({} as any), + validateOptions: (_options: JsonObject) => Promise.resolve({} as T), getBuilderNameForTarget: () => Promise.resolve(''), - analytics: null as any, getTargetOptions: (_: Target) => Promise.resolve({ outputPath: 'dist/some-folder' - }), + } as JsonObject), reportProgress: (_: number, __?: number, ___?: string) => {}, reportStatus: (_: string) => {}, reportRunning: () => {}, @@ -119,7 +173,8 @@ const initMocks = () => { Promise.resolve({ result: Promise.resolve(createBuilderOutputMock(true)) } as BuilderRun) - } as any; + }; + context = mockContext as BuilderContext; }; const createBuilderOutputMock = (success: boolean): BuilderOutput => { diff --git a/src/deploy/actions.ts b/src/deploy/actions.ts index 28be624..d43e4a1 100644 --- a/src/deploy/actions.ts +++ b/src/deploy/actions.ts @@ -2,7 +2,7 @@ import { BuilderContext, targetFromTargetString } from '@angular-devkit/architec import { logging } from '@angular-devkit/core'; import path from 'path'; -import { BuildTarget } from '../interfaces'; +import { BuildTarget, AngularOutputPath, isOutputPathObject } from '../interfaces'; import { Schema } from './schema'; export default async function deploy( @@ -60,29 +60,37 @@ export default async function deploy( ); // Output path configuration - // The outputPath option can be either + // The outputPath option can be either: + // - undefined (Angular 20+): uses default dist//browser // - a String which will be used as the base value + default value 'browser' // - or an Object for more fine-tune configuration. // see https://angular.io/guide/workspace-config#output-path-configuration // see https://github.com/angular/angular-cli/pull/26675 - if (!buildOptions.outputPath) { - throw new Error( - `Cannot read the outputPath option of the Angular project '${buildTarget.name}' in angular.json.` - ); - } + const outputPath = buildOptions.outputPath as AngularOutputPath | undefined; - if (typeof buildOptions.outputPath === 'string') { - dir = path.join(buildOptions.outputPath, 'browser'); + if (outputPath === undefined) { + // Angular 20+ default: dist//browser + // Extract project name from buildTarget.name (format: "project:target:configuration") + const projectName = buildTarget.name.split(':')[0]; + dir = path.join('dist', projectName, 'browser'); + } else if (typeof outputPath === 'string') { + dir = path.join(outputPath, 'browser'); + } else if (isOutputPathObject(outputPath)) { + // browser defaults to 'browser' per Angular CLI schema + // Explicit empty string '' means no subfolder (browser files directly in base) + dir = path.join(outputPath.base, outputPath.browser ?? 'browser'); } else { - const obj = buildOptions.outputPath as any; - dir = path.join(obj.base, obj.browser) + throw new Error( + `Unsupported outputPath configuration in angular.json for '${buildTarget.name}'. ` + + `Expected string or {base, browser} object.` + ); } } await engine.run( dir, options, - (context.logger as unknown) as logging.LoggerApi + context.logger ); } diff --git a/src/deploy/builder.spec.ts b/src/deploy/builder.spec.ts new file mode 100644 index 0000000..9bd9d4b --- /dev/null +++ b/src/deploy/builder.spec.ts @@ -0,0 +1,224 @@ +/** + * Tests for builder.ts - specifically testing executeDeploy function + * which contains the browserTarget rejection and error handling logic. + */ + +import { + BuilderContext, + BuilderRun, + ScheduleOptions, + Target +} from '@angular-devkit/architect/src'; +import { JsonObject, logging } from '@angular-devkit/core'; +import { Schema } from './schema'; + +// Mock the deploy function to prevent actual deployment +jest.mock('./actions', () => ({ + __esModule: true, + default: jest.fn().mockResolvedValue(undefined) +})); + +// Mock the engine module +jest.mock('../engine/engine', () => ({ + run: jest.fn().mockResolvedValue(undefined), + prepareOptions: jest.fn().mockImplementation((options) => Promise.resolve(options)) +})); + +// Import after mocking dependencies +import { executeDeploy } from './builder'; +import deployMock from './actions'; + +describe('builder.ts executeDeploy', () => { + let mockContext: BuilderContext; + let errorSpy: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + errorSpy = jest.fn(); + + mockContext = { + target: { + configuration: 'production', + project: 'test-project', + target: 'deploy' + }, + builder: { + builderName: 'angular-cli-ghpages:deploy', + description: 'Deploy to GitHub Pages', + optionSchema: false + }, + currentDirectory: '/test', + id: 1, + logger: { + error: errorSpy, + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + fatal: jest.fn(), + log: jest.fn(), + createChild: jest.fn() + } as unknown as logging.LoggerApi, + workspaceRoot: '/test', + addTeardown: jest.fn(), + validateOptions: jest.fn().mockResolvedValue({}), + getBuilderNameForTarget: jest.fn().mockResolvedValue(''), + getTargetOptions: jest.fn().mockResolvedValue({ outputPath: 'dist/test' } as JsonObject), + reportProgress: jest.fn(), + reportStatus: jest.fn(), + reportRunning: jest.fn(), + scheduleBuilder: jest.fn().mockResolvedValue({} as BuilderRun), + scheduleTarget: jest.fn().mockResolvedValue({ + result: Promise.resolve({ success: true }) + } as BuilderRun) + } as unknown as BuilderContext; + }); + + describe('browserTarget rejection', () => { + it('should reject browserTarget option with clear error message and return success: false', async () => { + const options = { browserTarget: 'test-project:build:production' } as unknown as Schema; + + const result = await executeDeploy(options, mockContext); + + expect(result.success).toBe(false); + expect(errorSpy).toHaveBeenCalledWith('❌ The "browserTarget" option is not supported.'); + expect(errorSpy).toHaveBeenCalledWith(' Use "buildTarget" instead.'); + }); + + it('should not call deploy when browserTarget is provided', async () => { + const options = { browserTarget: 'test-project:build:production' } as unknown as Schema; + + await executeDeploy(options, mockContext); + + expect(deployMock).not.toHaveBeenCalled(); + }); + + it('should proceed normally when buildTarget is used instead of browserTarget', async () => { + const options: Schema = { + buildTarget: 'test-project:build:production', + noBuild: true + }; + + const result = await executeDeploy(options, mockContext); + + expect(result.success).toBe(true); + expect(errorSpy).not.toHaveBeenCalled(); + expect(deployMock).toHaveBeenCalled(); + }); + }); + + describe('missing context.target', () => { + it('should throw error when context.target is undefined', async () => { + const contextWithoutTarget = { + ...mockContext, + target: undefined + } as unknown as BuilderContext; + + const options: Schema = { noBuild: true }; + + await expect(executeDeploy(options, contextWithoutTarget)).rejects.toThrow( + 'Cannot deploy the application without a target' + ); + }); + }); + + describe('deploy error handling', () => { + it('should catch deploy errors and return success: false with error message', async () => { + (deployMock as jest.Mock).mockRejectedValueOnce(new Error('Deployment failed')); + + const options: Schema = { noBuild: true }; + + const result = await executeDeploy(options, mockContext); + + expect(result.success).toBe(false); + expect(errorSpy).toHaveBeenCalledWith('❌ An error occurred when trying to deploy:'); + expect(errorSpy).toHaveBeenCalledWith('Deployment failed'); + }); + + it('should handle non-Error thrown values using String()', async () => { + (deployMock as jest.Mock).mockRejectedValueOnce('String error'); + + const options: Schema = { noBuild: true }; + + const result = await executeDeploy(options, mockContext); + + expect(result.success).toBe(false); + expect(errorSpy).toHaveBeenCalledWith('❌ An error occurred when trying to deploy:'); + expect(errorSpy).toHaveBeenCalledWith('String error'); + }); + + it('should handle object thrown values using String()', async () => { + (deployMock as jest.Mock).mockRejectedValueOnce({ code: 500, msg: 'Server error' }); + + const options: Schema = { noBuild: true }; + + const result = await executeDeploy(options, mockContext); + + expect(result.success).toBe(false); + expect(errorSpy).toHaveBeenCalledWith('[object Object]'); + }); + }); + + describe('build target resolution', () => { + it('should use prerenderTarget when provided', async () => { + const options: Schema = { + prerenderTarget: 'test-project:prerender:production', + noBuild: true + }; + + await executeDeploy(options, mockContext); + + expect(deployMock).toHaveBeenCalledWith( + expect.anything(), + mockContext, + { name: 'test-project:prerender:production' }, + options + ); + }); + + it('should use buildTarget when provided (no prerenderTarget)', async () => { + const options: Schema = { + buildTarget: 'test-project:build:staging', + noBuild: true + }; + + await executeDeploy(options, mockContext); + + expect(deployMock).toHaveBeenCalledWith( + expect.anything(), + mockContext, + { name: 'test-project:build:staging' }, + options + ); + }); + + it('should use default build target when neither prerenderTarget nor buildTarget provided', async () => { + const options: Schema = { noBuild: true }; + + await executeDeploy(options, mockContext); + + expect(deployMock).toHaveBeenCalledWith( + expect.anything(), + mockContext, + { name: 'test-project:build:production' }, + options + ); + }); + + it('should prefer prerenderTarget over buildTarget when both provided', async () => { + const options: Schema = { + buildTarget: 'test-project:build:production', + prerenderTarget: 'test-project:prerender:production', + noBuild: true + }; + + await executeDeploy(options, mockContext); + + expect(deployMock).toHaveBeenCalledWith( + expect.anything(), + mockContext, + { name: 'test-project:prerender:production' }, + options + ); + }); + }); +}); diff --git a/src/deploy/builder.ts b/src/deploy/builder.ts index ce88745..b1c42cd 100644 --- a/src/deploy/builder.ts +++ b/src/deploy/builder.ts @@ -9,53 +9,64 @@ import deploy from './actions'; import { Schema } from './schema'; import { BuildTarget } from '../interfaces'; +/** + * The core builder handler function. + * Exported separately for testing purposes. + */ +export async function executeDeploy( + options: Schema, + context: BuilderContext +): Promise { + // browserTarget is not supported - use buildTarget instead + if ((options as Record).browserTarget) { + context.logger.error('❌ The "browserTarget" option is not supported.'); + context.logger.error(' Use "buildTarget" instead.'); + return { success: false }; + } + + if (!context.target) { + throw new Error('Cannot deploy the application without a target'); + } + + const staticBuildTarget: BuildTarget = { + name: + options.buildTarget || `${context.target.project}:build:production` + }; + + let prerenderBuildTarget: BuildTarget | undefined; + if (options.prerenderTarget) { + prerenderBuildTarget = { + name: options.prerenderTarget + }; + } + + // serverBuildTarget is not supported and is completely ignored + // let serverBuildTarget: BuildTarget | undefined; + // if (options.ssr) { + // serverBuildTarget = { + // name: options.serverTarget || options.universalBuildTarget || `${context.target.project}:server:production` + // }; + // } + + const finalBuildTarget = prerenderBuildTarget + ? prerenderBuildTarget + : staticBuildTarget; + + try { + await deploy(engine, context, finalBuildTarget, options); + } catch (e) { + context.logger.error('❌ An error occurred when trying to deploy:'); + const message = e instanceof Error ? e.message : String(e); + context.logger.error(message); + return { success: false }; + } + + return { success: true }; +} + // Call the createBuilder() function to create a builder. This mirrors // createJobHandler() but add typings specific to Architect Builders. // // if something breaks here, see how angularfire has fixed it: // https://github.com/angular/angularfire/blob/master/src/schematics/deploy/builder.ts -export default createBuilder( - async (options: Schema, context: BuilderContext): Promise => { - if (!context.target) { - throw new Error('Cannot deploy the application without a target'); - } - - const staticBuildTarget = { - name: - options.browserTarget || - options.buildTarget || - `${context.target.project}:build:production` - }; - - let prerenderBuildTarget: BuildTarget | undefined; - if (options.prerender) { - prerenderBuildTarget = { - name: - options.prerenderTarget || - `${context.target.project}:prerender:production` - }; - } - - // serverBuildTarget is not supported and is completely ignored - // let serverBuildTarget: BuildTarget | undefined; - // if (options.ssr) { - // serverBuildTarget = { - // name: options.serverTarget || options.universalBuildTarget || `${context.target.project}:server:production` - // }; - // } - - const finalBuildTarget = prerenderBuildTarget - ? prerenderBuildTarget - : staticBuildTarget; - - try { - await deploy(engine, context, finalBuildTarget, options); - } catch (e) { - context.logger.error('❌ An error occurred when trying to deploy:'); - context.logger.error(e.message); - return { success: false }; - } - - return { success: true }; - } -); +export default createBuilder(executeDeploy); diff --git a/src/deploy/schema.d.ts b/src/deploy/schema.d.ts new file mode 100644 index 0000000..117a754 --- /dev/null +++ b/src/deploy/schema.d.ts @@ -0,0 +1,85 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Deployment of Angular CLI applications to GitHub pages (angular-cli-ghpages) + */ +export interface Schema { + /** + * Base url for the application being built. Same as `ng build --base-href=/XXX/`. + */ + baseHref?: string; + /** + * A named build target, as specified in the `configurations` section of angular.json. Each named target is accompanied by a configuration of option defaults for that target. This is equivalent to calling the command `ng build --configuration=XXX`. + */ + buildTarget?: string; + /** + * Architect target for prerendering/SSG. Takes precedence over buildTarget when specified. Requires a prerender target configured in angular.json. + */ + prerenderTarget?: string; + /** + * Skip build process during deployment. + */ + noBuild?: boolean; + /** + * Provide the remote name. If no value is provided, `origin` is used. Has no function if --repo is set. + */ + remote?: string; + /** + * Provide the repository URL. If no value is provided, a remote of the current working directory is used (defaults to `origin`; see --remote for details). + */ + repo?: string; + /** + * The commit message. + */ + message?: string; + /** + * The git branch to push your pages to. + */ + branch?: string; + /** + * The git user-name which is associated with this commit. + */ + name?: string; + /** + * The git user-email which is associated with this commit. + */ + email?: string; + /** + * Deprecated! This parameter is no longer needed. It will be ignored. + */ + noSilent?: boolean; + /** + * Exclude dotfiles (files starting with `.`) from deployment. + */ + noDotfiles?: boolean; + /** + * By default a 404.html file is created for SPA routing on GitHub Pages. For Cloudflare Pages, you must use --no-notfound to enable native SPA routing. + */ + noNotfound?: boolean; + /** + * Skip creating the .nojekyll file. + */ + noNojekyll?: boolean; + /** + * Generate a CNAME file for the specified domain. + */ + cname?: string; + /** + * Only add, and never remove existing files from the GitHub pages branch. + */ + add?: boolean; + /** + * Overrides the directory for all published sources, relative to the current working directory. + */ + dir?: string; + /** + * For testing: Run through without making any changes. Execute with --dry-run and nothing will happen. + */ + dryRun?: boolean; + [k: string]: unknown; +} diff --git a/src/deploy/schema.json b/src/deploy/schema.json index 2274f23..c4cad1f 100644 --- a/src/deploy/schema.json +++ b/src/deploy/schema.json @@ -11,13 +11,9 @@ "type": "string", "description": "A named build target, as specified in the `configurations` section of angular.json. Each named target is accompanied by a configuration of option defaults for that target. This is equivalent to calling the command `ng build --configuration=XXX`." }, - "browserTarget": { - "type": "string", - "description": "A named build target, as specified in the `configurations` section of angular.json. Each named target is accompanied by a configuration of option defaults for that target. This is equivalent to calling the command `ng build --configuration=XXX`." - }, "prerenderTarget": { "type": "string", - "description": "A named build target, as specified in the `configurations` section of angular.json. Each named target is accompanied by a configuration of option defaults for that target. This is equivalent to calling the command `ng build --configuration=XXX`." + "description": "Architect target for prerendering/SSG. Takes precedence over buildTarget when specified. Requires a prerender target configured in angular.json." }, "noBuild": { "type": "boolean", @@ -58,17 +54,17 @@ }, "noDotfiles": { "type": "boolean", - "description": "Includes dotfiles by default. Execute with --no-dotfiles to ignore files starting with `.`.", + "description": "Exclude dotfiles (files starting with `.`) from deployment.", "default": false }, "noNotfound": { "type": "boolean", - "description": "By default a 404.html file is created, because this is the only known workaround to avoid 404 error messages on GitHub Pages. For Cloudflare Pages we highly recommend to disable the 404.html file by setting this switch to true!", + "description": "By default a 404.html file is created for SPA routing on GitHub Pages. For Cloudflare Pages, you must use --no-notfound to enable native SPA routing.", "default": false }, "noNojekyll": { "type": "boolean", - "description": "By default a .nojekyll file is created, because we assume you don't want to compile the build again with Jekyll.", + "description": "Skip creating the .nojekyll file.", "default": false }, "cname": { diff --git a/src/engine/engine-filesystem.spec.ts b/src/engine/engine-filesystem.spec.ts new file mode 100644 index 0000000..3ed8cda --- /dev/null +++ b/src/engine/engine-filesystem.spec.ts @@ -0,0 +1,311 @@ +/** + * Real filesystem integration tests for file creation + * + * These tests use actual temporary directories to verify that: + * - 404.html is copied from index.html (handled by angular-cli-ghpages) + * - Dry-run mode prevents file creation + * - Error handling works as expected + * + * Note: CNAME and .nojekyll files are now handled by gh-pages v6+ via options. + * See "gh-pages v6 delegation" tests below for verification. + */ + +import { logging } from '@angular-devkit/core'; +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; + +import * as engine from './engine'; +import { cleanupMonkeypatch } from './engine.prepare-options-helpers'; + +describe('engine - real filesystem tests', () => { + const logger = new logging.Logger('test'); + let testDir: string; + let loggerInfoSpy: jest.SpyInstance; + + beforeEach(async () => { + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + + // Create a unique temp directory for each test + const tmpBase = os.tmpdir(); + const uniqueDir = `angular-cli-ghpages-test-${Date.now()}-${Math.random().toString(36).substring(7)}`; + testDir = path.join(tmpBase, uniqueDir); + await fse.ensureDir(testDir); + + // Spy on logger to capture warnings + loggerInfoSpy = jest.spyOn(logger, 'info'); + }); + + afterEach(async () => { + // Clean up temp directory after each test + if (await fse.pathExists(testDir)) { + await fse.remove(testDir); + } + loggerInfoSpy.mockRestore(); + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + }); + + describe('404.html file creation', () => { + it('should create 404.html as exact copy of index.html when notfound is true', async () => { + // First create an index.html file + const indexPath = path.join(testDir, 'index.html'); + const indexContent = 'Test

Test App

'; + await fse.writeFile(indexPath, indexContent); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + jest.spyOn(ghpages, 'publish').mockResolvedValue(undefined); + + const options = { + notfound: true, + nojekyll: false, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + const notFoundPath = path.join(testDir, '404.html'); + const exists = await fse.pathExists(notFoundPath); + expect(exists).toBe(true); + + const notFoundContent = await fse.readFile(notFoundPath, 'utf-8'); + expect(notFoundContent).toBe(indexContent); + }); + + it('should NOT create 404.html when notfound is false', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'Test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + jest.spyOn(ghpages, 'publish').mockResolvedValue(undefined); + + const options = { + notfound: false, + nojekyll: false, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + const notFoundPath = path.join(testDir, '404.html'); + const exists = await fse.pathExists(notFoundPath); + expect(exists).toBe(false); + }); + + it('should gracefully continue when index.html does not exist (not throw error)', async () => { + // No index.html created - directory is empty + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + jest.spyOn(ghpages, 'publish').mockResolvedValue(undefined); + + const options = { + notfound: true, + nojekyll: false, + dotfiles: true + }; + + // Should NOT throw - this is the critical test for graceful handling + await expect( + engine.run(testDir, options, logger) + ).resolves.toBeUndefined(); + + // Should log a warning message + expect(loggerInfoSpy).toHaveBeenCalledWith( + 'index.html could not be copied to 404.html. Proceeding without it.' + ); + + const notFoundPath = path.join(testDir, '404.html'); + const exists = await fse.pathExists(notFoundPath); + expect(exists).toBe(false); + }); + + it('should NOT create 404.html when dry-run is true', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'Test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + const options = { + notfound: true, + nojekyll: false, + dotfiles: true, + dryRun: true + }; + + await engine.run(testDir, options, logger); + + const notFoundPath = path.join(testDir, '404.html'); + const exists = await fse.pathExists(notFoundPath); + expect(exists).toBe(false); + }); + }); + + /** + * gh-pages v6+ Delegation Tests + * + * gh-pages v6.1.0 added native support for creating CNAME and .nojekyll files: + * - See: https://github.com/tschaub/gh-pages/pull/533 + * + * We now delegate file creation to gh-pages via the cname/nojekyll options + * instead of creating them ourselves. This is cleaner and avoids duplication. + * + * What we're testing: + * - Verify we DO pass cname option to gh-pages when provided + * - Verify we DO pass nojekyll option to gh-pages when enabled + * - Verify 404.html is still created by us (gh-pages doesn't handle this) + */ + describe('gh-pages v6 delegation - cname and nojekyll', () => { + it('should pass cname option to gh-pages when provided', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + let capturedOptions: { cname?: string; nojekyll?: boolean } = {}; + const publishSpy = jest.spyOn(ghpages, 'publish').mockImplementation( + (dir: string, options: { cname?: string; nojekyll?: boolean }) => { + capturedOptions = options; + return Promise.resolve(); + } + ); + + const testDomain = 'example.com'; + const options = { + cname: testDomain, + nojekyll: false, + notfound: false, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + expect(publishSpy).toHaveBeenCalled(); + expect(capturedOptions.cname).toBe(testDomain); + }); + + it('should pass nojekyll option to gh-pages when enabled', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + let capturedOptions: { cname?: string; nojekyll?: boolean } = {}; + const publishSpy = jest.spyOn(ghpages, 'publish').mockImplementation( + (dir: string, options: { cname?: string; nojekyll?: boolean }) => { + capturedOptions = options; + return Promise.resolve(); + } + ); + + const options = { + nojekyll: true, + notfound: false, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + expect(publishSpy).toHaveBeenCalled(); + expect(capturedOptions.nojekyll).toBe(true); + }); + + it('should pass both cname and nojekyll options when both enabled', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + let capturedOptions: { cname?: string; nojekyll?: boolean } = {}; + const publishSpy = jest.spyOn(ghpages, 'publish').mockImplementation( + (dir: string, options: { cname?: string; nojekyll?: boolean }) => { + capturedOptions = options; + return Promise.resolve(); + } + ); + + const testDomain = 'test.example.com'; + const options = { + cname: testDomain, + nojekyll: true, + notfound: true, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + expect(publishSpy).toHaveBeenCalled(); + expect(capturedOptions.cname).toBe(testDomain); + expect(capturedOptions.nojekyll).toBe(true); + + // Verify 404.html is still created by us (not delegated to gh-pages) + const notFoundPath = path.join(testDir, '404.html'); + expect(await fse.pathExists(notFoundPath)).toBe(true); + }); + + it('should NOT pass cname when not provided (undefined)', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + let capturedOptions: { cname?: string; nojekyll?: boolean } = {}; + const publishSpy = jest.spyOn(ghpages, 'publish').mockImplementation( + (dir: string, options: { cname?: string; nojekyll?: boolean }) => { + capturedOptions = options; + return Promise.resolve(); + } + ); + + const options = { + nojekyll: false, + notfound: false, + dotfiles: true + // cname not provided + }; + + await engine.run(testDir, options, logger); + + expect(publishSpy).toHaveBeenCalled(); + expect(capturedOptions.cname).toBeUndefined(); + }); + + it('should pass nojekyll: false when disabled', async () => { + const indexPath = path.join(testDir, 'index.html'); + await fse.writeFile(indexPath, 'test'); + + const ghpages = require('gh-pages'); + jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + + let capturedOptions: { cname?: string; nojekyll?: boolean } = {}; + const publishSpy = jest.spyOn(ghpages, 'publish').mockImplementation( + (dir: string, options: { cname?: string; nojekyll?: boolean }) => { + capturedOptions = options; + return Promise.resolve(); + } + ); + + const options = { + nojekyll: false, + notfound: false, + dotfiles: true + }; + + await engine.run(testDir, options, logger); + + expect(publishSpy).toHaveBeenCalled(); + expect(capturedOptions.nojekyll).toBe(false); + }); + }); +}); diff --git a/src/engine/engine.gh-pages-behavior.spec.ts b/src/engine/engine.gh-pages-behavior.spec.ts new file mode 100644 index 0000000..9682cc8 --- /dev/null +++ b/src/engine/engine.gh-pages-behavior.spec.ts @@ -0,0 +1,1064 @@ +/** + * Behavioral snapshot tests for gh-pages v6.3.0 + * + * These tests capture the EXACT internal behavior of gh-pages.publish(). + * Focus: Git commands executed in correct order with correct arguments. + * + * Purpose: When upgrading gh-pages, these tests will break if behavior changes. + * This ensures we maintain exact compatibility for our users. + * + * Approach: Mock child_process and gh-pages/lib/util, use real filesystem + */ + +import { ChildProcess } from 'child_process'; + +const path = require('path'); +const fs = require('fs-extra'); +const os = require('os'); +const { EventEmitter } = require('events'); + +// Types for better type safety +interface SpawnCall { + cmd: string; + args: string[]; + options: unknown; +} + +interface TestContext { + repo?: string; +} + +// Mock child_process to capture all git commands +let spawnCalls: SpawnCall[] = []; + +// Track current test context for deterministic mock behavior +let currentTestContext: TestContext = {}; + +// Factory function to create mock child process compatible with gh-pages expectations +// gh-pages lib/git.js expects: child.stdout.on, child.stderr.on, child.on('close') +function createMockChildProcess(): Partial { + const child: Partial = new EventEmitter(); + child.stdout = new EventEmitter() as unknown as ChildProcess['stdout']; + child.stderr = new EventEmitter() as unknown as ChildProcess['stderr']; + return child; +} + +/** + * Whitelist of expected git commands from gh-pages v6.3.0 + * + * Strict whitelist: If gh-pages changes git subcommands in future versions, + * this array must be updated first and tests will fail loudly. + * This is intentional - we want to know about any new git operations. + */ +const EXPECTED_GIT_COMMANDS = [ + 'clone', 'clean', 'fetch', 'checkout', 'ls-remote', 'reset', + 'rm', 'add', 'config', 'diff-index', 'commit', 'tag', 'push', 'update-ref' +]; + +const mockSpawn = jest.fn((cmd: string, args: string[] | undefined, opts: unknown) => { + const capturedArgs = args || []; + spawnCalls.push({ cmd, args: capturedArgs, options: opts }); + + // Validate git commands are expected + if (cmd === 'git' && capturedArgs[0]) { + if (!EXPECTED_GIT_COMMANDS.includes(capturedArgs[0])) { + throw new Error(`Unexpected git command: ${capturedArgs[0]}. Add to whitelist if intentional.`); + } + } + + const mockChild = createMockChildProcess(); + + // Simulate appropriate response based on git command + setImmediate(() => { + let output = ''; + + // git config --get remote.X.url should return the repo URL + if (cmd === 'git' && capturedArgs[0] === 'config' && capturedArgs[1] === '--get' && + capturedArgs[2] && capturedArgs[2].startsWith('remote.')) { + // Use tracked repo URL for deterministic behavior + output = currentTestContext.repo || ''; + } + // ls-remote should return something for branch existence check + else if (cmd === 'git' && capturedArgs[0] === 'ls-remote') { + output = 'refs/heads/gh-pages'; + } + // diff-index for checking if commit needed (exit 1 means changes exist) + else if (cmd === 'git' && capturedArgs[0] === 'diff-index') { + // Return exit code 1 to indicate changes exist + mockChild.emit!('close', 1); + return; + } + + mockChild.stdout!.emit('data', Buffer.from(output)); + mockChild.emit!('close', 0); + }); + + return mockChild; +}); + +jest.mock('child_process', () => ({ + spawn: mockSpawn +})); + +// Mock gh-pages/lib/util to avoid file copy operations +const mockCopy = jest.fn(() => Promise.resolve()); +const mockGetUser = jest.fn(() => Promise.resolve(null)); + +jest.mock('gh-pages/lib/util', () => ({ + copy: mockCopy, + getUser: mockGetUser +})); + +// Require gh-pages after mocking +const ghPages = require('gh-pages'); + +// Helper to avoid duplicate error handling +function publishAndHandle( + basePath: string, + options: unknown, + done: jest.DoneCallback, + assertions: () => void +): void { + ghPages.publish(basePath, options, (err: Error | null) => { + if (err) { + done(err); + return; + } + try { + assertions(); + done(); + } catch (assertionError) { + done(assertionError); + } + }); +} + +describe('gh-pages v6.3.0 - behavioral snapshot', () => { + let tempDir: string; + let basePath: string; + + beforeAll(async () => { + // Create a real temp directory with test files + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gh-pages-test-')); + basePath = path.join(tempDir, 'dist'); + await fs.ensureDir(basePath); + + // Create test files (including dotfiles for testing dotfiles option) + await fs.writeFile(path.join(basePath, 'index.html'), 'test'); + await fs.writeFile(path.join(basePath, 'main.js'), 'console.log("test");'); + await fs.writeFile(path.join(basePath, 'styles.css'), 'body { }'); + await fs.writeFile(path.join(basePath, '.htaccess'), 'RewriteEngine On'); + }); + + afterAll(async () => { + // Clean up temp directory + await fs.remove(tempDir); + }); + + beforeEach(() => { + // Note: We DON'T call ghPages.clean() here because it interferes with test isolation + // Each test uses a unique repo URL which naturally isolates cache directories + + // Clear all mock calls and context + spawnCalls = []; + currentTestContext = {}; + mockSpawn.mockClear(); + mockCopy.mockClear(); + mockGetUser.mockClear(); + }); + + describe('Git command execution order', () => { + it('should execute critical git commands in sequence', (done) => { + const repo = 'https://github.com/test/order-test.git'; + const branch = 'gh-pages'; + currentTestContext.repo = repo; + + const options = { repo, branch, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + // Extract git commands in order + const gitCommands = spawnCalls + .filter(call => call.cmd === 'git') + .map(call => call.args[0]); + + // Verify critical commands appear + expect(gitCommands).toContain('clone'); + expect(gitCommands).toContain('add'); + expect(gitCommands).toContain('commit'); + expect(gitCommands).toContain('push'); + + // Verify critical ordering constraints (these MUST be in order) + const cloneIndex = gitCommands.indexOf('clone'); + const addIndex = gitCommands.indexOf('add'); + const commitIndex = gitCommands.indexOf('commit'); + const pushIndex = gitCommands.indexOf('push'); + + expect(cloneIndex).toBeGreaterThan(-1); + expect(addIndex).toBeGreaterThan(cloneIndex); // add after clone + expect(commitIndex).toBeGreaterThan(addIndex); // commit after add + expect(pushIndex).toBeGreaterThan(commitIndex); // push after commit + }); + }); + + it('should execute ls-remote before checkout', (done) => { + const repo = 'https://github.com/test/ls-remote-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const gitCommands = spawnCalls + .filter(call => call.cmd === 'git') + .map(call => call.args[0]); + + const lsRemoteIndex = gitCommands.indexOf('ls-remote'); + const checkoutIndex = gitCommands.indexOf('checkout'); + + // ls-remote checks if branch exists before checkout + expect(lsRemoteIndex).toBeGreaterThan(-1); + expect(checkoutIndex).toBeGreaterThan(-1); + expect(lsRemoteIndex).toBeLessThan(checkoutIndex); + }); + }); + }); + + describe('Git clone command', () => { + it('should clone with exact repository URL', (done) => { + const repo = 'https://github.com/angular-schule/test-repo.git'; + const branch = 'production'; + currentTestContext.repo = repo; + const options = { repo, branch, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const cloneCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'clone' + ); + + expect(cloneCall).toBeDefined(); + expect(cloneCall?.args).toContain(repo); + expect(cloneCall?.args).toContain('--branch'); + expect(cloneCall?.args).toContain(branch); + expect(cloneCall?.args).toContain('--single-branch'); + }); + }); + + /** + * Git clone depth controls shallow clone behavior. + * + * What is depth? + * - `--depth N` limits git clone history to N commits + * - gh-pages defaults to depth=1 (only latest commit, optimal for deployments) + * - Smaller depth = faster clones, less disk space, faster CI/CD + * + * Why angular-cli-ghpages does NOT expose this option: + * - We're deploying build artifacts (compiled Angular app) + * - We only care about the latest built version + * - History is in the gh-pages branch itself + * - depth=1 is perfect - no benefit to cloning more history + * + * What we're testing: + * - That gh-pages uses its default depth=1 + * - Exact argument position and value + * - This ensures optimal performance for all deployments + */ + it('should use default depth=1 for optimal performance', (done) => { + const repo = 'https://github.com/test/depth-test.git'; + const branch = 'gh-pages'; + currentTestContext.repo = repo; + + // Don't specify depth - rely on gh-pages default + const options = { repo, branch, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const cloneCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'clone' + ); + expect(cloneCall).toBeDefined(); + + // Find --depth flag and verify next argument is exactly 1 + const depthFlagIndex = cloneCall?.args.indexOf('--depth'); + expect(depthFlagIndex).toBeGreaterThan(-1); + expect(cloneCall?.args[depthFlagIndex! + 1]).toBe(1); + }); + }); + }); + + describe('Git add and commit', () => { + it('should add all files with dot notation', (done) => { + const repo = 'https://github.com/test/add-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const addCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'add' + ); + + expect(addCall).toBeDefined(); + expect(addCall?.args[1]).toBe('.'); // Exact position check + }); + }); + + it('should commit with exact message provided', (done) => { + const repo = 'https://github.com/test/commit-test.git'; + const message = 'Custom deployment message with special chars: Γ©mojis πŸš€'; + currentTestContext.repo = repo; + const options = { repo, message }; + + publishAndHandle(basePath, options, done, () => { + const commitCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'commit' + ); + + expect(commitCall).toBeDefined(); + expect(commitCall?.args).toContain('-m'); + const messageIndex = commitCall?.args.indexOf('-m'); + expect(commitCall?.args[messageIndex! + 1]).toBe(message); + }); + }); + }); + + /** + * Git push command + * + * What does angular-cli-ghpages do? + * - We DO NOT pass the 'history' option to gh-pages + * - gh-pages defaults to history: true (normal push, no --force) + * - We rely on this default behavior + * + * What we're testing: + * - Push command includes correct remote and branch + * - Push ALWAYS includes --tags flag (gh-pages behavior) + * - NO --force flag (because we don't pass history: false) + */ + describe('Git push command', () => { + it('should push to correct remote and branch with --tags flag', (done) => { + const repo = 'https://github.com/test/push-test.git'; + const branch = 'gh-pages'; + const remote = 'upstream'; + currentTestContext.repo = repo; + const options = { repo, branch, remote, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const pushCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'push' + ); + + expect(pushCall).toBeDefined(); + expect(pushCall?.args).toContain(remote); + expect(pushCall?.args).toContain(branch); + // gh-pages ALWAYS includes --tags in push (lib/git.js line 189) + expect(pushCall?.args).toContain('--tags'); + }); + }); + + it('should NOT use force push (we rely on gh-pages default history: true)', (done) => { + const repo = 'https://github.com/test/no-force-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const pushCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'push' + ); + + expect(pushCall).toBeDefined(); + // We don't pass history: false, so gh-pages defaults to history: true (no --force) + expect(pushCall?.args).not.toContain('--force'); + }); + }); + }); + + describe('File copy operation', () => { + it('should copy exact test files from basePath to cache destination', (done) => { + const repo = 'https://github.com/test/copy-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + expect(mockCopy).toHaveBeenCalledTimes(1); + + const callArgs = mockCopy.mock.calls[0]; + expect(callArgs).toBeDefined(); + expect(callArgs.length).toBe(3); + + const [files, source, destination] = callArgs as unknown as [string[], string, string]; + + // Verify files array contains EXACTLY our test files + expect(Array.isArray(files)).toBe(true); + expect(files.length).toBe(3); + expect(files).toContain('index.html'); + expect(files).toContain('main.js'); + expect(files).toContain('styles.css'); + + // Verify source is our basePath + expect(source).toBe(basePath); + + // Verify destination is in gh-pages cache + expect(destination).toContain('gh-pages'); + }); + }); + }); + + /** + * Git rm behavior with add option + * + * What is the add option? + * - add: false (default) - Removes existing files before deploying new ones (clean slate) + * - add: true - Adds files without removing existing ones (incremental deployment) + * + * Why this matters: + * - add: false ensures no stale files remain (recommended for most cases) + * - add: true useful for multi-source deployments or preserving manually added files + * + * Critical behavior from gh-pages source (lib/index.js lines 156-170): + * if (options.add) { + * return git; // SKIP removal logic entirely + * } + * // Otherwise: execute globby to find files, then git rm if files exist + * + * Testing strategy: + * - We can't directly test git rm is called because it depends on existing files in cache + * - Instead, we test command ORDERING which differs between add: true and add: false + * - With add: false, checkout happens BEFORE add (removal step in between, even if no files) + * - With add: true, checkout happens BEFORE add (no removal step at all) + * - We also verify that add: true NEVER calls git rm (critical) + */ + describe('Git rm command behavior', () => { + it('should maintain standard command order with add: false (default)', (done) => { + const repo = 'https://github.com/test/rm-default-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', add: false }; + + publishAndHandle(basePath, options, done, () => { + const gitCommands = spawnCalls + .filter(call => call.cmd === 'git') + .map(call => call.args[0]); + + // Verify standard flow: checkout β†’ (removal logic) β†’ add β†’ commit + const checkoutIndex = gitCommands.indexOf('checkout'); + const addIndex = gitCommands.indexOf('add'); + const commitIndex = gitCommands.indexOf('commit'); + + expect(checkoutIndex).toBeGreaterThan(-1); + expect(addIndex).toBeGreaterThan(checkoutIndex); + expect(commitIndex).toBeGreaterThan(addIndex); + + // Note: git rm may or may not be called depending on existing files + // The important thing is the removal logic is ATTEMPTED (not skipped) + }); + }); + + it('should NEVER execute git rm when add: true (skips removal entirely)', (done) => { + const repo = 'https://github.com/test/rm-add-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', add: true }; + + publishAndHandle(basePath, options, done, () => { + const rmCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'rm' + ); + + // CRITICAL: git rm MUST NEVER be called when add: true + // This verifies the removal logic is completely skipped + expect(rmCall).toBeUndefined(); + }); + }); + }); + + /** + * Dotfiles option + * + * What does angular-cli-ghpages do? + * - We PASS dotfiles option to gh-pages (engine.ts line 242) + * - Default in angular-cli-ghpages: dotfiles: true (include dotfiles) + * - User can disable with --no-dotfiles flag + * + * How gh-pages uses it: + * - Controls globby pattern matching for files to copy + * - dotfiles: true β†’ includes files starting with '.' (like .htaccess) + * - dotfiles: false β†’ ignores files starting with '.' + * + * Why we test this: + * - CRITICAL: We expose this option and pass it to gh-pages + * - Must verify it affects file selection behavior + * + * Test fixture has these files: + * - index.html (normal file) + * - main.js (normal file) + * - styles.css (normal file) + * - .htaccess (dotfile) + */ + describe('Dotfiles option', () => { + it('should include dotfiles when dotfiles: true (our default)', (done) => { + const repo = 'https://github.com/test/dotfiles-true.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', dotfiles: true }; + + publishAndHandle(basePath, options, done, () => { + // gh-pages uses globby with { dot: options.dotfiles } (lib/index.js line 90) + expect(mockCopy).toHaveBeenCalledTimes(1); + + const callArgs = mockCopy.mock.calls[0]; + expect(callArgs).toBeDefined(); + expect(callArgs.length).toBe(3); + + const [files, source, destination] = callArgs as unknown as [string[], string, string]; + + // CRITICAL: Verify files array INCLUDES the .htaccess dotfile + expect(Array.isArray(files)).toBe(true); + expect(files.length).toBe(4); // index.html, main.js, styles.css, .htaccess + expect(files).toContain('index.html'); + expect(files).toContain('main.js'); + expect(files).toContain('styles.css'); + expect(files).toContain('.htaccess'); // This is the dotfile + + expect(source).toBe(basePath); + expect(destination).toContain('gh-pages'); + }); + }); + + it('should exclude dotfiles when dotfiles: false', (done) => { + const repo = 'https://github.com/test/dotfiles-false.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', dotfiles: false }; + + publishAndHandle(basePath, options, done, () => { + expect(mockCopy).toHaveBeenCalledTimes(1); + + const callArgs = mockCopy.mock.calls[0]; + expect(callArgs).toBeDefined(); + expect(callArgs.length).toBe(3); + + const [files, source, destination] = callArgs as unknown as [string[], string, string]; + + // CRITICAL: Verify files array EXCLUDES the .htaccess dotfile + expect(Array.isArray(files)).toBe(true); + expect(files.length).toBe(3); // Only index.html, main.js, styles.css + expect(files).toContain('index.html'); + expect(files).toContain('main.js'); + expect(files).toContain('styles.css'); + expect(files).not.toContain('.htaccess'); // Dotfile must be excluded + + expect(source).toBe(basePath); + expect(destination).toContain('gh-pages'); + }); + }); + }); + + /** + * Git executable option + * + * What does angular-cli-ghpages do? + * - We PASS git option to gh-pages (engine.ts line 240) + * - Default: 'git' (relies on PATH) + * - gh-pages defaults.git = 'git' (lib/index.js line 29) + * + * Why we test this: + * - We expose this option through defaults.ts + * - Must verify the option flows through to gh-pages + * + * Testing approach: + * - We pass git: 'git' (our default) + * - Verify git commands are executed + * - This confirms the option is accepted by gh-pages + */ + describe('Git executable option', () => { + it('should accept git executable option (we pass our default "git")', (done) => { + const repo = 'https://github.com/test/git-exe-test.git'; + const gitExecutable = 'git'; // Our default from defaults.ts line 15 + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', git: gitExecutable }; + + publishAndHandle(basePath, options, done, () => { + // Find all git commands + const gitCalls = spawnCalls.filter(call => call.cmd === 'git'); + + // CRITICAL: Verify git commands were executed + // This confirms gh-pages accepted our git option + expect(gitCalls.length).toBeGreaterThan(0); + + // Verify critical commands were executed with the git executable + expect(gitCalls.some(call => call.args[0] === 'clone')).toBe(true); + expect(gitCalls.some(call => call.args[0] === 'add')).toBe(true); + expect(gitCalls.some(call => call.args[0] === 'commit')).toBe(true); + }); + }); + }); + + describe('User credentials', () => { + it('should configure git user.email with exact value', (done) => { + const repo = 'https://github.com/test/user-test.git'; + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', user: { name, email } }; + + publishAndHandle(basePath, options, done, () => { + const configEmailCall = spawnCalls.find(call => + call.cmd === 'git' && + call.args[0] === 'config' && + call.args[1] === 'user.email' + ); + + expect(configEmailCall).toBeDefined(); + expect(configEmailCall?.args[2]).toBe(email); // Exact position + }); + }); + + it('should configure git user.name with exact value', (done) => { + const repo = 'https://github.com/test/username-test.git'; + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', user: { name, email } }; + + publishAndHandle(basePath, options, done, () => { + const configNameCall = spawnCalls.find(call => + call.cmd === 'git' && + call.args[0] === 'config' && + call.args[1] === 'user.name' + ); + + expect(configNameCall).toBeDefined(); + expect(configNameCall?.args[2]).toBe(name); // Exact position + }); + }); + }); + + /** + * Git tag command - Verify we DON'T use tagging + * + * What does angular-cli-ghpages do? + * - We DO NOT pass the 'tag' option to gh-pages + * - We DO NOT expose tag option in schema.json + * - Therefore, no git tags should ever be created + * + * Why test this? + * - Ensures our deployments don't accidentally create tags + * - If gh-pages changes defaults, we'll catch it + */ + describe('Git tag command', () => { + it('should NEVER create git tags (we do not use tag option)', (done) => { + const repo = 'https://github.com/test/no-tag-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + publishAndHandle(basePath, options, done, () => { + const tagCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'tag' + ); + + // CRITICAL: We should NEVER create tags + expect(tagCall).toBeUndefined(); + }); + }); + }); + + /** + * gh-pages default options we DON'T pass + * + * These tests verify gh-pages default behavior for options we don't pass. + * This ensures we understand what gh-pages is doing on our behalf. + * + * Options we DON'T pass (gh-pages uses defaults): + * - push: true (default) - Always pushes to remote + * - dest: '.' (default) - Deploys to root of branch + * - src: '**\/*' (default) - Includes all files + * - remove: '.' (default) - Removes existing files (unless add: true) + * - silent: false (default) - gh-pages logs output (we used to have noSilent but it's DEPRECATED) + * + * Note on 'silent' option: + * - We have 'noSilent' in schema.json but it's DEPRECATED and IGNORED + * - We do NOT pass 'silent' to gh-pages + * - gh-pages defaults to silent: false (logging enabled) + * - This is what we want: verbose logging for deployments + */ + describe('gh-pages default options (we do not pass these)', () => { + it('should push to remote (gh-pages default push: true)', (done) => { + const repo = 'https://github.com/test/default-push-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + // We DON'T pass push option, gh-pages defaults to push: true + + publishAndHandle(basePath, options, done, () => { + const pushCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'push' + ); + + // CRITICAL: Push MUST be called (gh-pages default: push: true) + expect(pushCall).toBeDefined(); + }); + }); + + it('should use root destination (gh-pages default dest: ".")', (done) => { + const repo = 'https://github.com/test/default-dest-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + // We DON'T pass dest option, gh-pages defaults to dest: '.' + + publishAndHandle(basePath, options, done, () => { + // Copy destination should be at root of cache (not in subdirectory) + expect(mockCopy).toHaveBeenCalledTimes(1); + + const callArgs = mockCopy.mock.calls[0]; + const [files, source, destination] = callArgs as unknown as [string[], string, string]; + + // Destination should be in gh-pages cache, not in a subdirectory + expect(destination).toContain('gh-pages'); + // Should NOT contain additional path segments beyond cache dir + // (dest: '.' means deploy to root of branch) + }); + }); + + it('should include all files (gh-pages default src: "**/*")', (done) => { + const repo = 'https://github.com/test/default-src-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + // We DON'T pass src option, gh-pages defaults to src: '**/*' + + publishAndHandle(basePath, options, done, () => { + expect(mockCopy).toHaveBeenCalledTimes(1); + + const callArgs = mockCopy.mock.calls[0]; + const [files] = callArgs as unknown as [string[], string, string]; + + // All our test files should be included (src: '**/*' means all files) + expect(files.length).toBe(3); + expect(files).toContain('index.html'); + expect(files).toContain('main.js'); + expect(files).toContain('styles.css'); + }); + }); + + it('should use default remove pattern (gh-pages default remove: ".")', (done) => { + const repo = 'https://github.com/test/default-remove-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy', add: false }; + // We DON'T pass remove option, gh-pages defaults to remove: '.' + // With add: false, gh-pages will attempt to remove files matching '.' + + publishAndHandle(basePath, options, done, () => { + // Since it's a fresh clone, there may be no files to remove + // But we verify the removal logic is executed (not skipped like with add: true) + const gitCommands = spawnCalls + .filter(call => call.cmd === 'git') + .map(call => call.args[0]); + + // Standard flow includes removal logic (even if no files to remove) + expect(gitCommands).toContain('checkout'); + expect(gitCommands).toContain('add'); + }); + }); + }); + + /** + * Error scenarios + * + * gh-pages has robust error handling. We test critical failure paths: + * 1. Git clone failure and retry (branch doesn't exist) + * 2. No changes to commit (diff-index returns 0) + * 3. Git commit when no user configured (uses getUser) + * + * Why test error scenarios: + * - Ensures deployments don't fail silently + * - Verifies retry/fallback mechanisms work + * - Documents expected error recovery behavior + */ + describe('Error scenarios', () => { + it('should retry git clone without branch/depth options on failure', (done) => { + const repo = 'https://github.com/test/clone-failure-test.git'; + currentTestContext.repo = repo; + const branch = 'new-branch'; + const options = { repo, branch, message: 'Deploy' }; + + // Reset mock to implement failure behavior + mockSpawn.mockClear(); + + let cloneAttempts = 0; + mockSpawn.mockImplementation((cmd: string, args: string[] | undefined, opts: unknown) => { + const capturedArgs = args || []; + spawnCalls.push({ cmd, args: capturedArgs, options: opts }); + + // Validate git commands are expected + if (cmd === 'git' && capturedArgs[0]) { + if (!EXPECTED_GIT_COMMANDS.includes(capturedArgs[0])) { + throw new Error(`Unexpected git command: ${capturedArgs[0]}`); + } + } + + const mockChild = createMockChildProcess(); + + setImmediate(() => { + // First clone attempt with --branch and --depth should FAIL + if (cmd === 'git' && capturedArgs[0] === 'clone' && cloneAttempts === 0) { + cloneAttempts++; + mockChild.stderr!.emit('data', Buffer.from('fatal: Remote branch new-branch not found')); + mockChild.emit!('close', 128); // Git error code + return; + } + + // Second clone attempt without --branch/--depth should SUCCEED + if (cmd === 'git' && capturedArgs[0] === 'clone' && cloneAttempts === 1) { + cloneAttempts++; + mockChild.stdout!.emit('data', Buffer.from('')); + mockChild.emit!('close', 0); + return; + } + + // All other commands succeed normally + let output = ''; + if (cmd === 'git' && capturedArgs[0] === 'config' && capturedArgs[1] === '--get') { + output = repo; + } else if (cmd === 'git' && capturedArgs[0] === 'ls-remote') { + output = 'refs/heads/gh-pages'; + } else if (cmd === 'git' && capturedArgs[0] === 'diff-index') { + mockChild.emit!('close', 1); + return; + } + + mockChild.stdout!.emit('data', Buffer.from(output)); + mockChild.emit!('close', 0); + }); + + return mockChild; + }); + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err) { + done(err); + return; + } + + try { + const cloneCalls = spawnCalls.filter(call => + call.cmd === 'git' && call.args[0] === 'clone' + ); + + // CRITICAL: Must have exactly 2 clone attempts + expect(cloneCalls.length).toBe(2); + + // First attempt: with --branch and --depth + expect(cloneCalls[0].args).toContain('--branch'); + expect(cloneCalls[0].args).toContain(branch); + expect(cloneCalls[0].args).toContain('--depth'); + + // Second attempt: WITHOUT --branch and --depth (fallback) + expect(cloneCalls[1].args).not.toContain('--branch'); + expect(cloneCalls[1].args).not.toContain('--depth'); + expect(cloneCalls[1].args).toContain(repo); + + done(); + } catch (assertionError) { + done(assertionError); + } + }); + }); + + it('should NOT commit when no changes exist (diff-index returns 0)', (done) => { + const repo = 'https://github.com/test/no-changes-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + + // Reset mock to implement no-changes behavior + mockSpawn.mockClear(); + spawnCalls = []; + + mockSpawn.mockImplementation((cmd: string, args: string[] | undefined, opts: unknown) => { + const capturedArgs = args || []; + spawnCalls.push({ cmd, args: capturedArgs, options: opts }); + + if (cmd === 'git' && capturedArgs[0]) { + if (!EXPECTED_GIT_COMMANDS.includes(capturedArgs[0])) { + throw new Error(`Unexpected git command: ${capturedArgs[0]}`); + } + } + + const mockChild = createMockChildProcess(); + + setImmediate(() => { + let output = ''; + + if (cmd === 'git' && capturedArgs[0] === 'config' && capturedArgs[1] === '--get') { + output = repo; + } else if (cmd === 'git' && capturedArgs[0] === 'ls-remote') { + output = 'refs/heads/gh-pages'; + } else if (cmd === 'git' && capturedArgs[0] === 'diff-index') { + // Return 0 = no changes (opposite of our normal mock) + mockChild.emit!('close', 0); + return; + } + + mockChild.stdout!.emit('data', Buffer.from(output)); + mockChild.emit!('close', 0); + }); + + return mockChild; + }); + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err) { + done(err); + return; + } + + try { + const commitCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'commit' + ); + + // CRITICAL: Commit should NOT be called when no changes + // gh-pages uses: git diff-index --quiet HEAD || git commit -m "message" + expect(commitCall).toBeUndefined(); + + // But push should still be called (even with no new commits) + const pushCall = spawnCalls.find(call => + call.cmd === 'git' && call.args[0] === 'push' + ); + expect(pushCall).toBeDefined(); + + done(); + } catch (assertionError) { + done(assertionError); + } + }); + }); + + it('should handle deployment without user credentials (uses getUser)', (done) => { + const repo = 'https://github.com/test/no-user-test.git'; + currentTestContext.repo = repo; + const options = { repo, message: 'Deploy' }; + // DON'T pass user option - gh-pages will call getUser() + + // Reset mock to normal behavior (previous test modified it) + mockSpawn.mockClear(); + spawnCalls = []; + + mockSpawn.mockImplementation((cmd: string, args: string[] | undefined, opts: unknown) => { + const capturedArgs = args || []; + spawnCalls.push({ cmd, args: capturedArgs, options: opts }); + + if (cmd === 'git' && capturedArgs[0]) { + if (!EXPECTED_GIT_COMMANDS.includes(capturedArgs[0])) { + throw new Error(`Unexpected git command: ${capturedArgs[0]}`); + } + } + + const mockChild = createMockChildProcess(); + + setImmediate(() => { + let output = ''; + + if (cmd === 'git' && capturedArgs[0] === 'config' && capturedArgs[1] === '--get' && + capturedArgs[2] && capturedArgs[2].startsWith('remote.')) { + output = currentTestContext.repo || ''; + } else if (cmd === 'git' && capturedArgs[0] === 'ls-remote') { + output = 'refs/heads/gh-pages'; + } else if (cmd === 'git' && capturedArgs[0] === 'diff-index') { + mockChild.emit!('close', 1); + return; + } + + mockChild.stdout!.emit('data', Buffer.from(output)); + mockChild.emit!('close', 0); + }); + + return mockChild; + }); + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err) { + done(err); + return; + } + + try { + // Verify getUser was called (our mock returns null) + expect(mockGetUser).toHaveBeenCalled(); + + // Verify NO git config commands for user.name/user.email + const configUserCalls = spawnCalls.filter(call => + call.cmd === 'git' && + call.args[0] === 'config' && + (call.args[1] === 'user.name' || call.args[1] === 'user.email') + ); + + // When getUser returns null, gh-pages skips git config + // (relies on global/local git config) + expect(configUserCalls.length).toBe(0); + + done(); + } catch (assertionError) { + done(assertionError); + } + }); + }); + + }); + + /** + * gh-pages v6+ CNAME and .nojekyll file creation + * + * IMPORTANT: File creation tests are in engine.gh-pages-filecreation.spec.ts + * Those tests run WITHOUT mocks to verify actual file creation behavior. + * + * This mocked test file cannot test actual file creation because: + * - We mock child_process.spawn (git commands) + * - gh-pages creates files in git.cwd which is set during clone + * - Since clone is mocked, the directory structure isn't created + * + * See engine.gh-pages-filecreation.spec.ts for real file creation tests. + */ + + /** + * gh-pages.clean() behavior + * + * What does clean() do? + * - Removes repo-specific cache subdirectories inside the gh-pages cache folder + * - Cache location: find-cache-dir determines base location (usually node_modules/.cache/gh-pages) + * - Structure: {cache-dir}/{filenamified-repo-url}/ + * - Source: gh-pages lib/index.js + * + * Why this matters: + * - Fixes "Remote url mismatch" errors + * - Clears stale git state + * - angular-cli-ghpages calls this before every deployment + * + * Note: clean() removes repo-specific subdirectories, not the entire cache parent directory + */ + describe('gh-pages.clean() behavior', () => { + it('should execute clean() without throwing errors', () => { + // clean() synchronously removes repo-specific cache directories + // This test verifies it doesn't throw - actual deletion is repo-specific + expect(() => ghPages.clean()).not.toThrow(); + }); + + it('should remove repo-specific cache directory', async () => { + const findCacheDir = require('find-cache-dir'); + const filenamify = require('filenamify'); + + const cacheBaseDir = findCacheDir({ name: 'gh-pages' }); + if (!cacheBaseDir) { + // Skip if no cache dir available (e.g., in some CI environments) + return; + } + + // Create a fake repo-specific cache directory + const fakeRepoUrl = 'https://github.com/test/clean-test-repo.git'; + const repoCacheDir = path.join(cacheBaseDir, filenamify(fakeRepoUrl, { replacement: '!' })); + + await fs.ensureDir(repoCacheDir); + await fs.writeFile(path.join(repoCacheDir, 'marker.txt'), 'should be deleted'); + expect(await fs.pathExists(repoCacheDir)).toBe(true); + + // Execute clean + ghPages.clean(); + + // Give filesystem time to process deletion + await new Promise(resolve => setTimeout(resolve, 100)); + + // Repo-specific directory should be removed + const existsAfter = await fs.pathExists(repoCacheDir); + expect(existsAfter).toBe(false); + }); + }); +}); diff --git a/src/engine/engine.gh-pages-filecreation.spec.ts b/src/engine/engine.gh-pages-filecreation.spec.ts new file mode 100644 index 0000000..2685d2a --- /dev/null +++ b/src/engine/engine.gh-pages-filecreation.spec.ts @@ -0,0 +1,257 @@ +/** + * gh-pages v6+ file creation tests (REAL filesystem with local git repo) + * + * These tests verify that gh-pages ACTUALLY creates CNAME and .nojekyll files + * on the REAL filesystem when the options are passed. + * + * IMPORTANT: These tests do NOT mock child_process or fs. + * This ensures we test the actual gh-pages behavior, not mocked behavior. + * + * Approach: + * - Create a local bare git repository for each test + * - gh-pages clones from the local repo (no network required) + * - Use beforeAdd callback to verify files before git add + * - Abort before push to avoid side effects + * + * Why this matters: + * - We delegated CNAME/.nojekyll creation from angular-cli-ghpages to gh-pages v6+ + * - We must verify gh-pages actually creates these files + * - Unlike 404.html (which we still create ourselves), these files are gh-pages' responsibility + */ + +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import { execSync } from 'child_process'; + +// NO MOCKS - we want real gh-pages behavior +const ghPages = require('gh-pages'); +const filenamify = require('filenamify'); + +describe('gh-pages v6+ CNAME/.nojekyll file creation (REAL filesystem)', () => { + let tempDir: string; + let basePath: string; + let bareRepoPath: string; + let cacheBaseDir: string; + + // Unique ID to prevent conflicts with parallel test runs + const testRunId = `${Date.now()}-${Math.random().toString(36).substring(7)}`; + + beforeAll(async () => { + // Get the cache directory that gh-pages will use + const findCacheDir = require('find-cache-dir'); + cacheBaseDir = findCacheDir({ name: 'gh-pages' }); + }); + + beforeEach(async () => { + // Create a unique temp directory for this test + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), `gh-pages-file-test-${testRunId}-`)); + + // Create source directory with minimal files + basePath = path.join(tempDir, 'dist'); + await fs.ensureDir(basePath); + await fs.writeFile(path.join(basePath, 'index.html'), 'test'); + + // Create a local bare git repository that gh-pages can clone from + bareRepoPath = path.join(tempDir, 'bare-repo.git'); + execSync(`git init --bare "${bareRepoPath}"`, { stdio: 'pipe' }); + + // Initialize gh-pages branch in the bare repo + // gh-pages needs the branch to exist, so we create it with an initial commit + const initWorkDir = path.join(tempDir, 'init-work'); + await fs.ensureDir(initWorkDir); + execSync(`git init "${initWorkDir}"`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" config user.email "test@test.com"`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" config user.name "Test"`, { stdio: 'pipe' }); + await fs.writeFile(path.join(initWorkDir, 'README.md'), 'init'); + execSync(`git -C "${initWorkDir}" add .`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" commit -m "init"`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" branch gh-pages`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" remote add origin "${bareRepoPath}"`, { stdio: 'pipe' }); + execSync(`git -C "${initWorkDir}" push -u origin gh-pages`, { stdio: 'pipe' }); + + // Clean gh-pages cache before each test + ghPages.clean(); + }); + + afterEach(async () => { + // Cleanup temp directory + await fs.remove(tempDir); + // Clean gh-pages cache after each test + ghPages.clean(); + }); + + /** + * Helper to get the cache directory path for a repo URL + * gh-pages uses filenamify to convert repo URL to directory name + */ + function getCacheDir(repoPath: string): string { + return path.join(cacheBaseDir, filenamify(repoPath, { replacement: '!' })); + } + + describe('CNAME file creation', () => { + it('should create CNAME file with exact domain content when cname option is set', (done) => { + const testDomain = 'test-cname.example.com'; + const cacheDir = getCacheDir(bareRepoPath); + + const options = { + repo: bareRepoPath, + branch: 'gh-pages', + cname: testDomain, + message: 'Test CNAME creation', + user: { name: 'Test', email: 'test@test.com' }, + // beforeAdd callback runs AFTER files are created but BEFORE git add + beforeAdd: async () => { + // CRITICAL: Verify CNAME file was created by gh-pages + const cnamePath = path.join(cacheDir, 'CNAME'); + + // Check file exists + const exists = await fs.pathExists(cnamePath); + expect(exists).toBe(true); + + // Check file has correct content + const content = await fs.readFile(cnamePath, 'utf-8'); + expect(content).toBe(testDomain); + + // Throw to abort publish (we don't want to actually push) + throw new Error('ABORT_TEST_SUCCESS'); + } + }; + + ghPages.publish(basePath, options, (err: Error | null) => { + // We expect an error because we threw in beforeAdd + if (err && err.message === 'ABORT_TEST_SUCCESS') { + done(); // Test passed - file was created correctly + } else if (err) { + done(err); // Unexpected error + } else { + done(new Error('Expected beforeAdd to abort, but publish succeeded')); + } + }); + }, 30000); // 30 second timeout for git operations + + it('should NOT create CNAME file when cname option is not provided', (done) => { + const cacheDir = getCacheDir(bareRepoPath); + + const options = { + repo: bareRepoPath, + branch: 'gh-pages', + // cname NOT provided + message: 'Test no CNAME', + user: { name: 'Test', email: 'test@test.com' }, + beforeAdd: async () => { + const cnamePath = path.join(cacheDir, 'CNAME'); + const exists = await fs.pathExists(cnamePath); + expect(exists).toBe(false); + throw new Error('ABORT_TEST_SUCCESS'); + } + }; + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err && err.message === 'ABORT_TEST_SUCCESS') { + done(); + } else if (err) { + done(err); + } else { + done(new Error('Expected beforeAdd to abort')); + } + }); + }, 30000); + }); + + describe('.nojekyll file creation', () => { + it('should create .nojekyll file when nojekyll option is true', (done) => { + const cacheDir = getCacheDir(bareRepoPath); + + const options = { + repo: bareRepoPath, + branch: 'gh-pages', + nojekyll: true, + message: 'Test nojekyll creation', + user: { name: 'Test', email: 'test@test.com' }, + beforeAdd: async () => { + const nojekyllPath = path.join(cacheDir, '.nojekyll'); + const exists = await fs.pathExists(nojekyllPath); + expect(exists).toBe(true); + throw new Error('ABORT_TEST_SUCCESS'); + } + }; + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err && err.message === 'ABORT_TEST_SUCCESS') { + done(); + } else if (err) { + done(err); + } else { + done(new Error('Expected beforeAdd to abort')); + } + }); + }, 30000); + + it('should NOT create .nojekyll file when nojekyll option is false', (done) => { + const cacheDir = getCacheDir(bareRepoPath); + + const options = { + repo: bareRepoPath, + branch: 'gh-pages', + nojekyll: false, + message: 'Test no nojekyll', + user: { name: 'Test', email: 'test@test.com' }, + beforeAdd: async () => { + const nojekyllPath = path.join(cacheDir, '.nojekyll'); + const exists = await fs.pathExists(nojekyllPath); + expect(exists).toBe(false); + throw new Error('ABORT_TEST_SUCCESS'); + } + }; + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err && err.message === 'ABORT_TEST_SUCCESS') { + done(); + } else if (err) { + done(err); + } else { + done(new Error('Expected beforeAdd to abort')); + } + }); + }, 30000); + }); + + describe('Both CNAME and .nojekyll together', () => { + it('should create both files when both options are set', (done) => { + const testDomain = 'both-files.example.com'; + const cacheDir = getCacheDir(bareRepoPath); + + const options = { + repo: bareRepoPath, + branch: 'gh-pages', + cname: testDomain, + nojekyll: true, + message: 'Test both files', + user: { name: 'Test', email: 'test@test.com' }, + beforeAdd: async () => { + // Verify CNAME + const cnamePath = path.join(cacheDir, 'CNAME'); + expect(await fs.pathExists(cnamePath)).toBe(true); + expect(await fs.readFile(cnamePath, 'utf-8')).toBe(testDomain); + + // Verify .nojekyll + const nojekyllPath = path.join(cacheDir, '.nojekyll'); + expect(await fs.pathExists(nojekyllPath)).toBe(true); + + throw new Error('ABORT_TEST_SUCCESS'); + } + }; + + ghPages.publish(basePath, options, (err: Error | null) => { + if (err && err.message === 'ABORT_TEST_SUCCESS') { + done(); + } else if (err) { + done(err); + } else { + done(new Error('Expected beforeAdd to abort')); + } + }); + }, 30000); + }); +}); diff --git a/src/engine/engine.gh-pages-integration.spec.ts b/src/engine/engine.gh-pages-integration.spec.ts new file mode 100644 index 0000000..fdbd495 --- /dev/null +++ b/src/engine/engine.gh-pages-integration.spec.ts @@ -0,0 +1,557 @@ +/** + * Integration tests for gh-pages library interaction + * + * These tests verify how engine.ts interacts with the gh-pages library: + * - When gh-pages.clean() is called + * - What arguments are passed to gh-pages.publish() + * - Which options are passed through vs. filtered out + * - Dry-run behavior isolation + * - Options transformation before passing to gh-pages + */ + +import { logging } from '@angular-devkit/core'; + +import * as engine from './engine'; +import { cleanupMonkeypatch } from './engine.prepare-options-helpers'; + +// Mock fs-extra at module level to avoid conflicts with other test files +jest.mock('fs-extra', () => ({ + pathExists: jest.fn().mockResolvedValue(true), + writeFile: jest.fn().mockResolvedValue(undefined), + copy: jest.fn().mockResolvedValue(undefined) +})); + +// Mock Git class from gh-pages to avoid spawning actual git processes +jest.mock('gh-pages/lib/git', () => { + return jest.fn().mockImplementation(() => ({ + getRemoteUrl: jest.fn().mockResolvedValue('https://github.com/test/repo.git') + })); +}); + +describe('engine - gh-pages integration', () => { + const logger = new logging.NullLogger(); + const originalEnv = process.env; + + // Only spy on gh-pages methods + let ghpagesCleanSpy: jest.SpyInstance; + let ghpagesPublishSpy: jest.SpyInstance; + + beforeEach(() => { + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + + const ghpages = require('gh-pages'); + + // Clear any existing mocks from previous tests + if (ghpagesCleanSpy) { + ghpagesCleanSpy.mockClear(); + } else { + ghpagesCleanSpy = jest.spyOn(ghpages, 'clean'); + } + + if (ghpagesPublishSpy) { + ghpagesPublishSpy.mockClear(); + } else { + ghpagesPublishSpy = jest.spyOn(ghpages, 'publish'); + } + + // Set default mock implementations - gh-pages v5+ uses Promise-based API + ghpagesCleanSpy.mockImplementation(() => {}); + ghpagesPublishSpy.mockResolvedValue(undefined); + + // Create fresh copy of environment for each test + // This preserves PATH, HOME, etc. needed by git + process.env = { ...originalEnv }; + // Clear only CI-specific vars we're testing + delete process.env.TRAVIS; + delete process.env.CIRCLECI; + delete process.env.GITHUB_ACTIONS; + delete process.env.GH_TOKEN; + delete process.env.PERSONAL_TOKEN; + delete process.env.GITHUB_TOKEN; + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + // Restore original environment for other test files + process.env = originalEnv; + }); + + describe('gh-pages.clean() behavior', () => { + it('should call clean() before publishing in normal mode', async () => { + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await engine.run(testDir, options, logger); + + expect(ghpagesCleanSpy).toHaveBeenCalledTimes(1); + expect(ghpagesCleanSpy).toHaveBeenCalledWith(); + }); + + it('should NOT call clean() during dry-run', async () => { + const testDir = '/test/dist'; + const options = { + dotfiles: true, + notfound: true, + nojekyll: true, + dryRun: true + }; + + await engine.run(testDir, options, logger); + + expect(ghpagesCleanSpy).not.toHaveBeenCalled(); + }); + + it('should call clean() before publish() to avoid remote url mismatch', async () => { + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await engine.run(testDir, options, logger); + + // Verify clean() was called before publish() + const cleanCallOrder = ghpagesCleanSpy.mock.invocationCallOrder[0]; + const publishCallOrder = ghpagesPublishSpy.mock.invocationCallOrder[0]; + + expect(cleanCallOrder).toBeLessThan(publishCallOrder); + }); + }); + + describe('gh-pages.publish() - directory parameter', () => { + it('should pass the correct directory path to publish()', async () => { + const testDir = '/test/dist/my-app'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await engine.run(testDir, options, logger); + + expect(ghpagesPublishSpy).toHaveBeenCalledTimes(1); + // gh-pages v5+ uses Promise-based API (no callback) + expect(ghpagesPublishSpy).toHaveBeenCalledWith( + testDir, + expect.any(Object) + ); + }); + + it('should pass different directory paths correctly', async () => { + const firstDir = '/first/path'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await engine.run(firstDir, options, logger); + + const actualDir = ghpagesPublishSpy.mock.calls[0][0]; + expect(actualDir).toBe(firstDir); + }); + }); + + describe('gh-pages.publish() - options parameter', () => { + it('should pass core options to gh-pages', async () => { + const testDir = '/test/dist'; + const repo = 'https://github.com/test/repo.git'; + const branch = 'main'; + const message = 'Deploy to GitHub Pages'; + const remote = 'upstream'; + + const options = { + repo, + branch, + message, + remote, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.repo).toBe(repo); + expect(actualOptions.branch).toBe(branch); + expect(actualOptions.message).toBe(message); + expect(actualOptions.remote).toBe(remote); + }); + + it('should pass transformed dotfiles boolean to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dotfiles).toBe(true); + }); + + it('should pass dotfiles: false when transformed from noDotfiles: true', async () => { + const testDir = '/test/dist'; + const options = { + noDotfiles: true, + dotfiles: true, // will be overwritten by noDotfiles transformation + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dotfiles).toBe(false); + }); + + it('should pass user credentials object when both name and email provided', async () => { + const testDir = '/test/dist'; + const userName = 'CI Bot'; + const userEmail = 'ci@example.com'; + + const options = { + name: userName, + email: userEmail, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.user).toEqual({ + name: userName, + email: userEmail + }); + }); + + it('should NOT have user object when only name is provided', async () => { + const testDir = '/test/dist'; + const options = { + name: 'CI Bot', + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.user).toBeUndefined(); + }); + + it('should pass add option for incremental deployment', async () => { + const testDir = '/test/dist'; + const options = { + add: true, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.add).toBe(true); + }); + + it('should pass git option to gh-pages', async () => { + const testDir = '/test/dist'; + const gitPath = '/custom/path/to/git'; + + const options = { + git: gitPath, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.git).toBe(gitPath); + }); + + it('should NOT pass internal dryRun option to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + dryRun: false, // Internal option, should be filtered out + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dryRun).toBeUndefined(); + }); + + it('should NOT pass internal noDotfiles option to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noDotfiles: false, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.noDotfiles).toBeUndefined(); + }); + + it('should NOT pass internal noNotfound option to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noNotfound: false, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.noNotfound).toBeUndefined(); + }); + + it('should NOT pass internal noNojekyll option to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noNojekyll: false, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.noNojekyll).toBeUndefined(); + }); + + it('should NOT pass internal notfound option to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + dotfiles: true, + notfound: true, // Internal only, used for 404.html creation + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + // notfound remains internal - 404.html is still created by angular-cli-ghpages + expect(actualOptions.notfound).toBeUndefined(); + }); + + it('should pass nojekyll option to gh-pages v6+', async () => { + const testDir = '/test/dist'; + const options = { + dotfiles: true, + notfound: true, + nojekyll: true // gh-pages v6+ handles .nojekyll file creation + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + // nojekyll IS now passed to gh-pages v6+ (delegated file creation) + expect(actualOptions.nojekyll).toBe(true); + }); + + it('should pass cname option to gh-pages v6+', async () => { + const testDir = '/test/dist'; + const options = { + cname: 'example.com', // gh-pages v6+ handles CNAME file creation + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + // cname IS now passed to gh-pages v6+ (delegated file creation) + expect(actualOptions.cname).toBe('example.com'); + }); + }); + + describe('gh-pages.publish() - dry-run mode isolation', () => { + it('should NOT call publish() during dry-run', async () => { + const testDir = '/test/dist'; + const options = { + dryRun: true, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + expect(ghpagesPublishSpy).not.toHaveBeenCalled(); + }); + + it('should NOT call clean() during dry-run', async () => { + const testDir = '/test/dist'; + const options = { + dryRun: true, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + expect(ghpagesCleanSpy).not.toHaveBeenCalled(); + }); + + it('should log what WOULD be published during dry-run', async () => { + const testLogger = new logging.Logger('test'); + const infoSpy = jest.spyOn(testLogger, 'info'); + + const testDir = '/test/dist'; + const repo = 'https://github.com/test/repo.git'; + const branch = 'gh-pages'; + + const options = { + dryRun: true, + repo, + branch, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, testLogger); + + // Should log the dry-run preview + expect(infoSpy).toHaveBeenCalledWith( + expect.stringContaining("Dry-run / SKIPPED: publishing folder") + ); + expect(infoSpy).toHaveBeenCalledWith( + expect.stringContaining(testDir) + ); + }); + }); + + describe('options transformation verification', () => { + it('should transform noDotfiles: true to dotfiles: false before passing to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noDotfiles: true, + dotfiles: true, // will be overwritten + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dotfiles).toBe(false); + expect(actualOptions.noDotfiles).toBeUndefined(); + }); + + it('should transform noDotfiles: false to dotfiles: true before passing to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noDotfiles: false, + dotfiles: false, // will be overwritten + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dotfiles).toBe(true); + expect(actualOptions.noDotfiles).toBeUndefined(); + }); + + it('should use default dotfiles: true when no noDotfiles is provided', async () => { + const testDir = '/test/dist'; + const options = { + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + expect(actualOptions.dotfiles).toBe(true); + }); + + it('should NOT pass transformed noNotfound/notfound to gh-pages', async () => { + const testDir = '/test/dist'; + const options = { + noNotfound: true, + dotfiles: true, + notfound: true, // will be overwritten to false + nojekyll: true + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + // notfound remains internal - 404.html is still created by angular-cli-ghpages + expect(actualOptions.notfound).toBeUndefined(); + expect(actualOptions.noNotfound).toBeUndefined(); + }); + + it('should pass transformed noNojekyll to nojekyll: false to gh-pages v6+', async () => { + const testDir = '/test/dist'; + const options = { + noNojekyll: true, + dotfiles: true, + notfound: true, + nojekyll: true // will be overwritten to false + }; + + await engine.run(testDir, options, logger); + + const actualOptions = ghpagesPublishSpy.mock.calls[0][1] as Record; + // nojekyll IS now passed to gh-pages v6+ (delegated file creation) + expect(actualOptions.nojekyll).toBe(false); + expect(actualOptions.noNojekyll).toBeUndefined(); + }); + }); + + describe('Promise handling integration', () => { + // gh-pages v5+ uses Promise-based API (we no longer use callback-based approach) + + it('should invoke gh-pages.publish() without callback (Promise-based)', async () => { + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await engine.run(testDir, options, logger); + + // gh-pages v5+ Promise API: publish(dir, options) - no callback + expect(ghpagesPublishSpy).toHaveBeenCalledWith( + expect.any(String), + expect.any(Object) + ); + }); + + it('should resolve when gh-pages.publish() resolves', async () => { + ghpagesPublishSpy.mockResolvedValue(undefined); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).resolves.toBeUndefined(); + }); + + it('should reject when gh-pages.publish() rejects', async () => { + const publishError = new Error('Git push failed'); + ghpagesPublishSpy.mockRejectedValue(publishError); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).rejects.toThrow('Git push failed'); + }); + }); +}); diff --git a/src/engine/engine.prepare-options-helpers.spec.ts b/src/engine/engine.prepare-options-helpers.spec.ts new file mode 100644 index 0000000..b386c98 --- /dev/null +++ b/src/engine/engine.prepare-options-helpers.spec.ts @@ -0,0 +1,658 @@ +/** + * Intensive tests for prepareOptions helper functions + * + * These tests provide 100% coverage of all option transformation logic + * by testing each helper function independently. + */ + +import * as path from 'path'; +import { logging } from '@angular-devkit/core'; + +import * as helpers from './engine.prepare-options-helpers'; +import { Schema } from '../deploy/schema'; + +describe('prepareOptions helpers - intensive tests', () => { + let testLogger: logging.Logger; + let infoSpy: jest.SpyInstance; + let warnSpy: jest.SpyInstance; + const originalEnv = process.env; + + beforeEach(() => { + testLogger = new logging.Logger('test'); + infoSpy = jest.spyOn(testLogger, 'info'); + warnSpy = jest.spyOn(testLogger, 'warn'); + // Create fresh copy of environment for each test + // This preserves PATH, HOME, etc. needed by git + process.env = { ...originalEnv }; + // Clear only CI-specific vars we're testing + delete process.env.TRAVIS; + delete process.env.TRAVIS_COMMIT; + delete process.env.TRAVIS_COMMIT_MESSAGE; + delete process.env.TRAVIS_REPO_SLUG; + delete process.env.TRAVIS_BUILD_WEB_URL; + delete process.env.CIRCLECI; + delete process.env.CIRCLE_PROJECT_USERNAME; + delete process.env.CIRCLE_PROJECT_REPONAME; + delete process.env.CIRCLE_SHA1; + delete process.env.CIRCLE_BUILD_URL; + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_REPOSITORY; + delete process.env.GITHUB_SHA; + delete process.env.GITHUB_RUN_ID; + delete process.env.GITHUB_SERVER_URL; + delete process.env.GH_TOKEN; + delete process.env.PERSONAL_TOKEN; + delete process.env.GITHUB_TOKEN; + }); + + afterEach(() => { + infoSpy.mockRestore(); + warnSpy.mockRestore(); + }); + + afterAll(() => { + // Restore original environment for other test files + process.env = originalEnv; + }); + + describe('setupMonkeypatch', () => { + let originalDebuglog: typeof import('util').debuglog; + + beforeEach(() => { + // First, clean up any previous monkeypatch state + helpers.cleanupMonkeypatch(); + const util = require('util'); + originalDebuglog = util.debuglog; + }); + + afterEach(() => { + // Use our cleanup function to properly restore state + helpers.cleanupMonkeypatch(); + }); + + it('should replace util.debuglog with custom implementation', () => { + const util = require('util'); + const debuglogBefore = util.debuglog; + + helpers.setupMonkeypatch(testLogger); + + expect(util.debuglog).not.toBe(debuglogBefore); + }); + + it('should forward gh-pages debuglog calls to logger', () => { + helpers.setupMonkeypatch(testLogger); + + const util = require('util'); + const ghPagesLogger = util.debuglog('gh-pages'); + const testMessage = 'Test gh-pages message'; + + ghPagesLogger(testMessage); + + expect(infoSpy).toHaveBeenCalledWith(testMessage); + }); + + it('should format messages with placeholders before forwarding', () => { + helpers.setupMonkeypatch(testLogger); + + const util = require('util'); + const ghPagesLogger = util.debuglog('gh-pages'); + + ghPagesLogger('Publishing %d files to %s branch', 42, 'gh-pages'); + + expect(infoSpy).toHaveBeenCalledWith('Publishing 42 files to gh-pages branch'); + }); + + it('should call original debuglog for non-gh-pages modules', () => { + const util = require('util'); + const originalDebuglogSpy = jest.fn(originalDebuglog); + util.debuglog = originalDebuglogSpy; + + helpers.setupMonkeypatch(testLogger); + + const otherLogger = util.debuglog('some-other-module'); + + expect(originalDebuglogSpy).toHaveBeenCalledWith('some-other-module'); + expect(infoSpy).not.toHaveBeenCalled(); + }); + }); + + describe('mapNegatedBooleans', () => { + it('should set dotfiles to false when noDotfiles is true', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = { noDotfiles: true }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.dotfiles).toBe(false); + }); + + it('should set dotfiles to true when noDotfiles is false', () => { + const options: helpers.PreparedOptions = { dotfiles: false, notfound: true, nojekyll: true }; + const origOptions: Schema = { noDotfiles: false }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.dotfiles).toBe(true); + }); + + it('should NOT modify dotfiles when noDotfiles is undefined', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = {}; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.dotfiles).toBe(true); + }); + + it('should set notfound to false when noNotfound is true', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = { noNotfound: true }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.notfound).toBe(false); + }); + + it('should set notfound to true when noNotfound is false', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: false, nojekyll: true }; + const origOptions: Schema = { noNotfound: false }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.notfound).toBe(true); + }); + + it('should NOT modify notfound when noNotfound is undefined', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = {}; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.notfound).toBe(true); + }); + + it('should set nojekyll to false when noNojekyll is true', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = { noNojekyll: true }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.nojekyll).toBe(false); + }); + + it('should set nojekyll to true when noNojekyll is false', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: false }; + const origOptions: Schema = { noNojekyll: false }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.nojekyll).toBe(true); + }); + + it('should NOT modify nojekyll when noNojekyll is undefined', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = {}; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.nojekyll).toBe(true); + }); + + it('should handle all three negated booleans simultaneously', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = { + noDotfiles: true, + noNotfound: true, + noNojekyll: true + }; + + helpers.mapNegatedBooleans(options, origOptions); + + expect(options.dotfiles).toBe(false); + expect(options.notfound).toBe(false); + expect(options.nojekyll).toBe(false); + }); + }); + + describe('handleUserCredentials', () => { + it('should create user object when both name and email are provided', () => { + const options: helpers.PreparedOptions = { + dotfiles: true, + notfound: true, + nojekyll: true, + name: 'John Doe', + email: 'john@example.com' + }; + const origOptions: Schema = {}; + + helpers.handleUserCredentials(options, origOptions, testLogger); + + expect(options['user']).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should warn when only name is provided', () => { + const options: helpers.PreparedOptions = { + dotfiles: true, + notfound: true, + nojekyll: true, + name: 'John Doe' + }; + const origOptions: Schema = {}; + const expectedWarning = 'WARNING: Both --name and --email must be set together to configure git user. Only --name is set. Git will use the local or global git config instead.'; + + helpers.handleUserCredentials(options, origOptions, testLogger); + + expect(options['user']).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should warn when only email is provided', () => { + const options: helpers.PreparedOptions = { + dotfiles: true, + notfound: true, + nojekyll: true, + email: 'john@example.com' + }; + const origOptions: Schema = {}; + const expectedWarning = 'WARNING: Both --name and --email must be set together to configure git user. Only --email is set. Git will use the local or global git config instead.'; + + helpers.handleUserCredentials(options, origOptions, testLogger); + + expect(options['user']).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should NOT warn or create user object when neither name nor email is provided', () => { + const options: helpers.PreparedOptions = { dotfiles: true, notfound: true, nojekyll: true }; + const origOptions: Schema = {}; + + helpers.handleUserCredentials(options, origOptions, testLogger); + + expect(options['user']).toBeUndefined(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); + + describe('warnDeprecatedParameters', () => { + it('should warn when noSilent is true', () => { + const origOptions: Schema = { noSilent: true }; + const expectedWarning = 'The --no-silent parameter is deprecated and no longer needed. Verbose logging is now always enabled. This parameter will be ignored.'; + + helpers.warnDeprecatedParameters(origOptions, testLogger); + + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should warn when noSilent is false', () => { + const origOptions: Schema = { noSilent: false }; + const expectedWarning = 'The --no-silent parameter is deprecated and no longer needed. Verbose logging is now always enabled. This parameter will be ignored.'; + + helpers.warnDeprecatedParameters(origOptions, testLogger); + + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should NOT warn when noSilent is undefined', () => { + const origOptions: Schema = {}; + + helpers.warnDeprecatedParameters(origOptions, testLogger); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); + + describe('appendCIMetadata', () => { + const baseMessage = 'Deploy to gh-pages'; + + it('should append Travis CI metadata with exact format', () => { + process.env.TRAVIS = 'true'; + process.env.TRAVIS_COMMIT_MESSAGE = 'Fix bug in component'; + process.env.TRAVIS_REPO_SLUG = 'user/repo'; + process.env.TRAVIS_COMMIT = 'abc123def456'; + process.env.TRAVIS_BUILD_WEB_URL = 'https://app.travis-ci.com/user/repo/builds/987654321'; + + const options: helpers.PreparedOptions = { + message: baseMessage, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + helpers.appendCIMetadata(options); + + const expectedMessage = + 'Deploy to gh-pages -- Fix bug in component \n\n' + + 'Triggered by commit: https://github.com/user/repo/commit/abc123def456\n' + + 'Travis CI build: https://app.travis-ci.com/user/repo/builds/987654321'; + + expect(options.message).toBe(expectedMessage); + }); + + it('should append CircleCI metadata with exact format', () => { + process.env.CIRCLECI = 'true'; + process.env.CIRCLE_PROJECT_USERNAME = 'johndoe'; + process.env.CIRCLE_PROJECT_REPONAME = 'myproject'; + process.env.CIRCLE_SHA1 = 'fedcba987654'; + process.env.CIRCLE_BUILD_URL = 'https://circleci.com/gh/johndoe/myproject/123'; + + const options: helpers.PreparedOptions = { + message: baseMessage, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + helpers.appendCIMetadata(options); + + const expectedMessage = + 'Deploy to gh-pages\n\n' + + 'Triggered by commit: https://github.com/johndoe/myproject/commit/fedcba987654\n' + + 'CircleCI build: https://circleci.com/gh/johndoe/myproject/123'; + + expect(options.message).toBe(expectedMessage); + }); + + it('should append GitHub Actions metadata with exact format', () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_REPOSITORY = 'angular-schule/angular-cli-ghpages'; + process.env.GITHUB_SHA = '1234567890abcdef'; + process.env.GITHUB_RUN_ID = '9876543210'; + + const options: helpers.PreparedOptions = { + message: baseMessage, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + helpers.appendCIMetadata(options); + + const expectedMessage = + 'Deploy to gh-pages\n\n' + + 'Triggered by commit: https://github.com/angular-schule/angular-cli-ghpages/commit/1234567890abcdef\n' + + 'GitHub Actions build: https://github.com/angular-schule/angular-cli-ghpages/actions/runs/9876543210'; + + expect(options.message).toBe(expectedMessage); + }); + + it('should NOT modify message when no CI env is set', () => { + const options: helpers.PreparedOptions = { + message: baseMessage, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + helpers.appendCIMetadata(options); + + expect(options.message).toBe(baseMessage); + }); + + it('should append metadata from multiple CI environments in correct order', () => { + // Set up multiple CI environments (Travis, CircleCI, GitHub Actions) + process.env.TRAVIS = 'true'; + process.env.TRAVIS_COMMIT_MESSAGE = 'Update docs'; + process.env.TRAVIS_REPO_SLUG = 'org/repo'; + process.env.TRAVIS_COMMIT = 'abc123'; + process.env.TRAVIS_BUILD_WEB_URL = 'https://app.travis-ci.com/org/repo/builds/111'; + + process.env.CIRCLECI = 'true'; + process.env.CIRCLE_PROJECT_USERNAME = 'org'; + process.env.CIRCLE_PROJECT_REPONAME = 'repo'; + process.env.CIRCLE_SHA1 = 'def456'; + process.env.CIRCLE_BUILD_URL = 'https://circleci.com/gh/org/repo/222'; + + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_REPOSITORY = 'org/repo'; + process.env.GITHUB_SHA = 'ghi789'; + + const options: helpers.PreparedOptions = { + message: baseMessage, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + helpers.appendCIMetadata(options); + + // Verify Travis appears first, then CircleCI, then GitHub Actions + expect(options.message).toBeDefined(); + const travisIndex = options.message!.indexOf('Travis CI build'); + const circleIndex = options.message!.indexOf('CircleCI build'); + const ghActionsIndex = options.message!.indexOf('Triggered by commit: https://github.com/org/repo/commit/ghi789'); + + expect(travisIndex).toBeGreaterThan(-1); + expect(circleIndex).toBeGreaterThan(travisIndex); + expect(ghActionsIndex).toBeGreaterThan(circleIndex); + }); + }); + + describe('injectTokenIntoRepoUrl', () => { + it('should inject GH_TOKEN into plain HTTPS URL', async () => { + const inputUrl = 'https://github.com/user/repo.git'; + const token = 'secret_gh_token_123'; + const expectedUrl = 'https://x-access-token:secret_gh_token_123@github.com/user/repo.git'; + + process.env.GH_TOKEN = token; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(expectedUrl); + }); + + it('should inject PERSONAL_TOKEN into plain HTTPS URL', async () => { + const inputUrl = 'https://github.com/user/repo.git'; + const token = 'personal_token_456'; + const expectedUrl = 'https://x-access-token:personal_token_456@github.com/user/repo.git'; + + process.env.PERSONAL_TOKEN = token; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(expectedUrl); + }); + + it('should inject GITHUB_TOKEN into plain HTTPS URL', async () => { + const inputUrl = 'https://github.com/user/repo.git'; + const token = 'github_token_789'; + const expectedUrl = 'https://x-access-token:github_token_789@github.com/user/repo.git'; + + process.env.GITHUB_TOKEN = token; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(expectedUrl); + }); + + it('should replace legacy GH_TOKEN placeholder', async () => { + const inputUrl = 'https://GH_TOKEN@github.com/user/repo.git'; + const token = 'my_token'; + const expectedUrl = 'https://my_token@github.com/user/repo.git'; + + process.env.GH_TOKEN = token; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(expectedUrl); + }); + + it('should NOT inject token if URL already contains x-access-token', async () => { + const inputUrl = 'https://x-access-token:existing_token@github.com/user/repo.git'; + + process.env.GH_TOKEN = 'new_token'; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(inputUrl); + }); + + it('should NOT inject token if URL is not HTTPS github.com', async () => { + const sshUrl = 'git@github.com:user/repo.git'; + + process.env.GH_TOKEN = 'token'; + + const options: helpers.PreparedOptions = { + repo: sshUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(sshUrl); + }); + + it('should handle token with special characters', async () => { + const inputUrl = 'https://github.com/user/repo.git'; + const token = 'token_with_$pecial_ch@rs!'; + const expectedUrl = 'https://x-access-token:token_with_$pecial_ch@rs!@github.com/user/repo.git'; + + process.env.GH_TOKEN = token; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(expectedUrl); + }); + + it('should NOT inject token when no token env variable is set', async () => { + const inputUrl = 'https://github.com/user/repo.git'; + + const options: helpers.PreparedOptions = { + repo: inputUrl, + dotfiles: true, + notfound: true, + nojekyll: true + }; + + await helpers.injectTokenIntoRepoUrl(options); + + expect(options.repo).toBe(inputUrl); + }); + + }); + + describe('getRemoteUrl - gh-pages/lib/git internal API', () => { + /** + * CRITICAL: This tests our dependency on gh-pages internal API + * + * These tests will BREAK if gh-pages changes: + * - gh-pages/lib/git module structure + * - Git class constructor signature + * - getRemoteUrl() method signature or behavior + * + * Testing approach: + * - We're IN a git repository, so getRemoteUrl() succeeds and returns a URL + * - We verify the function is callable and returns string URLs + * - We test error cases by using invalid parameters + * + * If these tests fail after upgrading gh-pages, see the WARNING + * comment in engine.prepare-options-helpers.ts for fallback options. + */ + + /** + * Environment assumptions for this test: + * - Tests must be run from a git clone of angular-schule/angular-cli-ghpages + * - The "origin" remote must exist and point to that repository + * - git must be installed and on PATH + * + * If run from a bare copy of files (no .git), this test will fail by design. + */ + it('should return correct URL from git config and be consistent', async () => { + // This test verifies the internal API returns ACTUAL git config values + const options = { remote: 'origin' }; + + // Get what git actually says + const { execSync } = require('child_process'); + const expectedUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8' }).trim(); + + // Verify getRemoteUrl returns the same value + const result = await helpers.getRemoteUrl(options); + expect(result).toBe(expectedUrl); + + // Verify consistency across multiple calls + const result2 = await helpers.getRemoteUrl(options); + expect(result2).toBe(expectedUrl); + expect(result).toBe(result2); + }); + + it('should throw helpful error for non-existent remote', async () => { + const options = { + remote: 'nonexistent-remote-12345' // Remote that definitely doesn't exist + }; + + // Expected message from gh-pages v6.3.0 (lib/git.js) + // If this fails after upgrading gh-pages, the internal API changed + await expect(helpers.getRemoteUrl(options)) + .rejects + .toThrow('Failed to get remote.nonexistent-remote-12345.url (task must either be run in a git repository with a configured nonexistent-remote-12345 remote or must be configured with the "repo" option).'); + }); + + it('should throw helpful error when not in a git repository', async () => { + // Change to a non-git directory + const originalCwd = process.cwd(); + const tempDir = path.join(require('os').tmpdir(), 'not-a-git-repo-test-' + Date.now()); + await require('fs-extra').ensureDir(tempDir); + + try { + process.chdir(tempDir); + const options = { remote: 'origin' }; + + // Expected message from gh-pages v6.3.0 (lib/git.js) + // Note: gh-pages returns same error for both "not in git repo" and "remote doesn't exist" + await expect(helpers.getRemoteUrl(options)) + .rejects + .toThrow('Failed to get remote.origin.url (task must either be run in a git repository with a configured origin remote or must be configured with the "repo" option).'); + } finally { + process.chdir(originalCwd); + await require('fs-extra').remove(tempDir); + } + }); + }); +}); diff --git a/src/engine/engine.prepare-options-helpers.ts b/src/engine/engine.prepare-options-helpers.ts new file mode 100644 index 0000000..9e71451 --- /dev/null +++ b/src/engine/engine.prepare-options-helpers.ts @@ -0,0 +1,256 @@ +/** + * Helper functions for prepareOptions + * + * These functions handle the various transformations and validations + * that prepareOptions performs on deployment options. + */ + +import { logging } from '@angular-devkit/core'; +import * as util from 'util'; + +import { Schema } from '../deploy/schema'; +// Internal API dependency - by design. See getRemoteUrl() JSDoc for rationale and fallback options. +import Git from 'gh-pages/lib/git'; + +/** + * Type for options with the three boolean flags that prepareOptions adds, + * plus the optional user object created from name + email + */ +export type PreparedOptions = Schema & { + dotfiles: boolean; + notfound: boolean; + nojekyll: boolean; + user?: { name: string; email: string }; +}; + +// Store original debuglog for cleanup (using CommonJS require for mutable access) +// eslint-disable-next-line @typescript-eslint/no-var-requires +const utilMutable = require('util'); +let originalDebuglog: typeof util.debuglog | null = null; + +/** + * Setup monkeypatch for util.debuglog to intercept gh-pages logging + * + * gh-pages uses util.debuglog('gh-pages') internally for all verbose logging. + * We intercept it and forward to Angular logger instead of stderr. + * + * CRITICAL: This must be called BEFORE requiring gh-pages, otherwise gh-pages + * will cache the original util.debuglog and our interception won't work. + * + * NOTE: We use require('util') instead of ES import because ES module namespace + * objects are read-only. CommonJS require returns a mutable object that we can patch. + */ +export function setupMonkeypatch(logger: logging.LoggerApi): void { + // Guard against multiple calls - only patch once + // If we're already patched, just return (prevents stack overflow from recursive calls) + if (originalDebuglog !== null) { + return; + } + + originalDebuglog = utilMutable.debuglog; + + utilMutable.debuglog = (set: string) => { + // gh-pages uses util.debuglog('gh-pages') internally for all verbose logging + // Intercept it and forward to Angular logger instead of stderr + if (set === 'gh-pages') { + return function (...args: unknown[]) { + const message = util.format.apply(util, args); + logger.info(message); + }; + } + return originalDebuglog!(set); + }; +} + +/** + * Cleanup monkeypatch - restore original util.debuglog + * Exported for testing + */ +export function cleanupMonkeypatch(): void { + if (originalDebuglog) { + utilMutable.debuglog = originalDebuglog; + originalDebuglog = null; + } +} + +/** + * Map negated boolean options to positive boolean options + * + * Angular-CLI is NOT renaming the vars, so noDotfiles, noNotfound, and noNojekyll + * come in with no change. We map this to dotfiles, notfound, nojekyll to have a + * consistent pattern between Commander and Angular-CLI. + */ +export function mapNegatedBooleans( + options: PreparedOptions, + origOptions: Schema +): void { + if (origOptions.noDotfiles !== undefined) { + options.dotfiles = !origOptions.noDotfiles; + } + if (origOptions.noNotfound !== undefined) { + options.notfound = !origOptions.noNotfound; + } + if (origOptions.noNojekyll !== undefined) { + options.nojekyll = !origOptions.noNojekyll; + } +} + +/** + * Handle user credentials - create user object or warn if only one is set + */ +export function handleUserCredentials( + options: PreparedOptions, + origOptions: Schema, + logger: logging.LoggerApi +): void { + if (options.name && options.email) { + options.user = { + name: options.name, + email: options.email + }; + } else if (options.name || options.email) { + logger.warn( + 'WARNING: Both --name and --email must be set together to configure git user. ' + + (options.name ? 'Only --name is set.' : 'Only --email is set.') + + ' Git will use the local or global git config instead.' + ); + } +} + +/** + * Warn if deprecated parameters are used + */ +export function warnDeprecatedParameters(origOptions: Schema, logger: logging.LoggerApi): void { + if (origOptions.noSilent !== undefined) { + logger.warn( + 'The --no-silent parameter is deprecated and no longer needed. ' + + 'Verbose logging is now always enabled. This parameter will be ignored.' + ); + } +} + +/** + * Append CI environment metadata to commit message + */ +export function appendCIMetadata(options: PreparedOptions): void { + if (process.env.TRAVIS) { + options.message += + ' -- ' + + (process.env.TRAVIS_COMMIT_MESSAGE || '') + + ' \n\n' + + 'Triggered by commit: https://github.com/' + + (process.env.TRAVIS_REPO_SLUG || '') + + '/commit/' + + (process.env.TRAVIS_COMMIT || '') + + '\n' + + 'Travis CI build: ' + + (process.env.TRAVIS_BUILD_WEB_URL || ''); + } + + if (process.env.CIRCLECI) { + options.message += + '\n\n' + + 'Triggered by commit: https://github.com/' + + (process.env.CIRCLE_PROJECT_USERNAME || '') + + '/' + + (process.env.CIRCLE_PROJECT_REPONAME || '') + + '/commit/' + + (process.env.CIRCLE_SHA1 || '') + + '\n' + + 'CircleCI build: ' + + (process.env.CIRCLE_BUILD_URL || ''); + } + + if (process.env.GITHUB_ACTIONS) { + options.message += + '\n\n' + + 'Triggered by commit: https://github.com/' + + (process.env.GITHUB_REPOSITORY || '') + + '/commit/' + + (process.env.GITHUB_SHA || '') + + '\n' + + 'GitHub Actions build: ' + + (process.env.GITHUB_SERVER_URL || 'https://github.com') + + '/' + + (process.env.GITHUB_REPOSITORY || '') + + '/actions/runs/' + + (process.env.GITHUB_RUN_ID || ''); + } +} + +/** + * Inject authentication token into repository URL + * + * Supports GH_TOKEN, PERSONAL_TOKEN, and GITHUB_TOKEN environment variables. + * Also handles legacy GH_TOKEN placeholder replacement for backwards compatibility. + */ +export async function injectTokenIntoRepoUrl(options: PreparedOptions): Promise { + // NEW in 0.6.2: always discover remote URL (if not set) + // this allows us to inject tokens from environment even if `--repo` is not set manually + if (!options.repo) { + options.repo = await getRemoteUrl(options); + } + + // for backwards compatibility only, + // in the past --repo=https://GH_TOKEN@github.com//.git was advised + // + // this replacement was also used to inject other tokens into the URL, + // so it should only be removed with the next major version + if ( + process.env.GH_TOKEN && + options.repo && + options.repo.includes('GH_TOKEN') + ) { + options.repo = options.repo.replace('GH_TOKEN', process.env.GH_TOKEN); + } + // preferred way: token is replaced from plain URL + // Note: Only the first available token is used (GH_TOKEN > PERSONAL_TOKEN > GITHUB_TOKEN) + else if (options.repo && !options.repo.includes('x-access-token:')) { + if (process.env.GH_TOKEN) { + options.repo = options.repo.replace( + 'https://github.com/', + `https://x-access-token:${process.env.GH_TOKEN}@github.com/` + ); + } else if (process.env.PERSONAL_TOKEN) { + options.repo = options.repo.replace( + 'https://github.com/', + `https://x-access-token:${process.env.PERSONAL_TOKEN}@github.com/` + ); + } else if (process.env.GITHUB_TOKEN) { + options.repo = options.repo.replace( + 'https://github.com/', + `https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/` + ); + } + } +} + +/** + * Get the remote URL from the git repository + * + * ⚠️ WARNING: This uses gh-pages internal API (gh-pages/lib/git) + * + * UPGRADE RISK: + * - This function depends on gh-pages/lib/git which is an internal module + * - Not part of gh-pages public API - could break in any version + * - When upgrading gh-pages, verify this still works: + * 1. Check if gh-pages/lib/git still exists + * 2. Check if Git class constructor signature is unchanged + * 3. Check if getRemoteUrl() method still exists and works + * + * FALLBACK OPTIONS if this breaks: + * - Option 1: Shell out to `git config --get remote.origin.url` directly + * - Option 2: Use a dedicated git library (simple-git, nodegit) + * - Option 3: Require users to always pass --repo explicitly + * + * IMPORTANT: This function expects options.remote to be set (our defaults provide 'origin') + * It should NOT be called with undefined remote, as gh-pages will convert it to string "undefined" + * + * Exported for testing - internal use only + */ +export async function getRemoteUrl(options: Schema & { git?: string; remote?: string }): Promise { + // process.cwd() returns the directory from which ng deploy was invoked. + // This is the expected behavior - users run ng deploy from their project root. + const git = new Git(process.cwd(), options.git); + return await git.getRemoteUrl(options.remote); +} diff --git a/src/engine/engine.spec.ts b/src/engine/engine.spec.ts index 85c12c9..cc510b5 100644 --- a/src/engine/engine.spec.ts +++ b/src/engine/engine.spec.ts @@ -1,13 +1,34 @@ import { logging } from '@angular-devkit/core'; import * as engine from './engine'; +import { cleanupMonkeypatch } from './engine.prepare-options-helpers'; describe('engine', () => { describe('prepareOptions', () => { const logger = new logging.NullLogger(); + const originalEnv = process.env; beforeEach(() => { - process.env = {}; + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + + // Create fresh copy of environment for each test + // This preserves PATH, HOME, etc. needed by git + process.env = { ...originalEnv }; + // Clear only CI-specific vars we're testing + delete process.env.TRAVIS; + delete process.env.CIRCLECI; + delete process.env.GITHUB_ACTIONS; + delete process.env.GH_TOKEN; + delete process.env.PERSONAL_TOKEN; + delete process.env.GITHUB_TOKEN; + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + // Restore original environment for other test files + process.env = originalEnv; }); it('should replace the string GH_TOKEN in the repo url (for backwards compatibility)', async () => { @@ -75,13 +96,25 @@ describe('engine', () => { }); // NEW in 0.6.2: always discover remote URL (if not set) + /** + * Environment assumptions for this test: + * - Tests must be run from a git clone of angular-schule/angular-cli-ghpages + * - The "origin" remote must exist and point to that repository + * - git must be installed and on PATH + * + * If run from a bare copy of files (no .git), this test will fail by design. + */ // this allows us to inject tokens from environment even if --repo is not set manually // it uses gh-pages lib directly for this it('should discover the remote url, if no --repo is set', async () => { const options = {}; const finalOptions = await engine.prepareOptions(options, logger); - expect(finalOptions.repo).toMatch(/angular-schule\/angular-cli-ghpages/); + // Justification for .toContain(): + // The protocol (SSH vs HTTPS) depends on developer's git config. + // Our testing philosophy allows .toContain() for substrings in long/variable messages. + // We only care that the correct repo path is discovered. + expect(finalOptions.repo).toContain('angular-schule/angular-cli-ghpages'); }); describe('remote', () => { @@ -126,4 +159,297 @@ describe('engine', () => { expect(finalOptions.nojekyll).toBe(true); }); }); + + describe('run - dist folder validation', () => { + const logger = new logging.NullLogger(); + + it('should throw error when dist folder does not exist', async () => { + // This test proves the CRITICAL operator precedence bug was fixed + // BUG: await !fse.pathExists(dir) - applies ! to Promise (always false, error NEVER thrown) + // FIX: !(await fse.pathExists(dir)) - awaits first, then negates (error IS thrown) + + // Mock gh-pages module + jest.mock('gh-pages', () => ({ + clean: jest.fn(), + publish: jest.fn() + })); + + const fse = require('fs-extra'); + jest.spyOn(fse, 'pathExists').mockResolvedValue(false); + + const nonExistentDir = '/path/to/nonexistent/dir'; + const expectedErrorMessage = 'Dist folder does not exist. Check the dir --dir parameter or build the project first!'; + + await expect( + engine.run(nonExistentDir, { dotfiles: true, notfound: true, nojekyll: true }, logger) + ).rejects.toThrow(expectedErrorMessage); + + expect(fse.pathExists).toHaveBeenCalledWith(nonExistentDir); + }); + }); + + describe('prepareOptions - user credentials warnings', () => { + it('should warn when only name is set without email', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = { name: 'John Doe' }; + const expectedWarning = 'WARNING: Both --name and --email must be set together to configure git user. Only --name is set. Git will use the local or global git config instead.'; + + await engine.prepareOptions(options, testLogger); + + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should warn when only email is set without name', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = { email: 'john@example.com' }; + const expectedWarning = 'WARNING: Both --name and --email must be set together to configure git user. Only --email is set. Git will use the local or global git config instead.'; + + await engine.prepareOptions(options, testLogger); + + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should NOT warn when both name and email are set', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = { name: 'John Doe', email: 'john@example.com' }; + + const finalOptions = await engine.prepareOptions(options, testLogger); + + expect(finalOptions.user).toEqual({ name: 'John Doe', email: 'john@example.com' }); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('name and --email must be set together')); + }); + }); + + describe('prepareOptions - deprecated noSilent warning', () => { + it('should warn when noSilent parameter is used', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = { noSilent: true }; + const expectedWarning = 'The --no-silent parameter is deprecated and no longer needed. Verbose logging is now always enabled. This parameter will be ignored.'; + + await engine.prepareOptions(options, testLogger); + + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + + it('should NOT warn when noSilent is not provided', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = {}; + + await engine.prepareOptions(options, testLogger); + + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('no-silent')); + }); + }); + + describe('run - gh-pages Promise error handling', () => { + // gh-pages v5+ supports Promise-based API (fixed the bug where early errors didn't reject) + // We now use await ghPages.publish() directly instead of callback-based approach + const logger = new logging.NullLogger(); + + let fsePathExistsSpy: jest.SpyInstance; + let fseWriteFileSpy: jest.SpyInstance; + let ghpagesCleanSpy: jest.SpyInstance; + let ghpagesPublishSpy: jest.SpyInstance; + + beforeEach(() => { + // Setup persistent mocks for fs-extra + const fse = require('fs-extra'); + fsePathExistsSpy = jest.spyOn(fse, 'pathExists').mockResolvedValue(true); + fseWriteFileSpy = jest.spyOn(fse, 'writeFile').mockResolvedValue(undefined); + + // Setup persistent mocks for gh-pages + const ghpages = require('gh-pages'); + ghpagesCleanSpy = jest.spyOn(ghpages, 'clean').mockImplementation(() => {}); + ghpagesPublishSpy = jest.spyOn(ghpages, 'publish'); + }); + + afterEach(() => { + // Clean up spies + fsePathExistsSpy.mockRestore(); + fseWriteFileSpy.mockRestore(); + ghpagesCleanSpy.mockRestore(); + ghpagesPublishSpy.mockRestore(); + }); + + it('should reject when gh-pages.publish rejects with error', async () => { + const publishError = new Error('Git push failed: permission denied'); + + // gh-pages v5+ properly rejects with errors + ghpagesPublishSpy.mockRejectedValue(publishError); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).rejects.toThrow('Git push failed: permission denied'); + }); + + it('should preserve error message through rejection', async () => { + const detailedError = new Error('Remote url mismatch. Expected https://github.com/user/repo.git but got https://github.com/other/repo.git'); + + ghpagesPublishSpy.mockRejectedValue(detailedError); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).rejects.toThrow(detailedError); + }); + + it('should reject with authentication error from gh-pages', async () => { + const authError = new Error('Authentication failed: Invalid credentials'); + + ghpagesPublishSpy.mockRejectedValue(authError); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).rejects.toThrow('Authentication failed: Invalid credentials'); + }); + + it('should resolve successfully when gh-pages.publish resolves', async () => { + // gh-pages v5+ resolves the Promise on success + ghpagesPublishSpy.mockResolvedValue(undefined); + + const testDir = '/test/dist'; + const options = { dotfiles: true, notfound: true, nojekyll: true }; + + await expect( + engine.run(testDir, options, logger) + ).resolves.toBeUndefined(); + }); + }); + + describe('prepareOptions - monkeypatch verification', () => { + beforeEach(() => { + // Clean up monkeypatch before each test to start fresh + cleanupMonkeypatch(); + }); + + afterEach(() => { + // Clean up monkeypatch after each test + cleanupMonkeypatch(); + }); + + it('should replace util.debuglog with custom implementation', async () => { + const testLogger = new logging.Logger('test'); + const util = require('util'); + const debuglogBeforePrepare = util.debuglog; + + await engine.prepareOptions({}, testLogger); + + // After prepareOptions, util.debuglog should be replaced + expect(util.debuglog).not.toBe(debuglogBeforePrepare); + }); + + it('should forward gh-pages debuglog calls to Angular logger', async () => { + const testLogger = new logging.Logger('test'); + const infoSpy = jest.spyOn(testLogger, 'info'); + + await engine.prepareOptions({}, testLogger); + + // Now get the patched debuglog for 'gh-pages' + const util = require('util'); + const ghPagesLogger = util.debuglog('gh-pages'); + + // Call it with a test message + const testMessage = 'Publishing to gh-pages branch'; + ghPagesLogger(testMessage); + + // Should have forwarded to logger.info() + expect(infoSpy).toHaveBeenCalledWith(testMessage); + }); + + it('should forward gh-pages debuglog calls with formatting to Angular logger', async () => { + const testLogger = new logging.Logger('test'); + const infoSpy = jest.spyOn(testLogger, 'info'); + + await engine.prepareOptions({}, testLogger); + + const util = require('util'); + const ghPagesLogger = util.debuglog('gh-pages'); + + // Test with util.format style placeholders + ghPagesLogger('Pushing %d files to %s', 42, 'gh-pages'); + + // Should format the message and forward to logger.info() + expect(infoSpy).toHaveBeenCalledWith('Pushing 42 files to gh-pages'); + }); + + it('should call original debuglog for non-gh-pages modules', async () => { + const testLogger = new logging.Logger('test'); + const infoSpy = jest.spyOn(testLogger, 'info'); + + const util = require('util'); + const originalDebuglogFn = util.debuglog; + const originalDebuglogSpy = jest.fn(originalDebuglogFn); + util.debuglog = originalDebuglogSpy; + + await engine.prepareOptions({}, testLogger); + + // Now util.debuglog is patched + const otherModuleLogger = util.debuglog('some-other-module'); + + // Should have called the original debuglog (via our spy) + expect(originalDebuglogSpy).toHaveBeenCalledWith('some-other-module'); + + // Should NOT have forwarded to Angular logger + expect(infoSpy).not.toHaveBeenCalled(); + }); + + it('should monkeypatch util.debuglog before requiring gh-pages', async () => { + // This test verifies the critical ordering requirement: + // The monkeypatch MUST occur before requiring gh-pages, otherwise gh-pages caches + // the original util.debuglog and our interception won't work. + + const testLogger = new logging.Logger('test'); + const infoSpy = jest.spyOn(testLogger, 'info'); + + // Clear gh-pages from require cache to simulate fresh load + const ghPagesPath = require.resolve('gh-pages'); + delete require.cache[ghPagesPath]; + + await engine.prepareOptions({}, testLogger); + + // Now require gh-pages for the first time (after monkeypatch) + require('gh-pages'); + + // Verify our patched debuglog('gh-pages') forwards to the logger + const util = require('util'); + const ghPagesLogger = util.debuglog('gh-pages'); + ghPagesLogger('test message'); + expect(infoSpy).toHaveBeenCalledWith('test message'); + }); + + it('should restore original util.debuglog when cleanupMonkeypatch is called', async () => { + const testLogger = new logging.Logger('test'); + const util = require('util'); + const debuglogBeforeSetup = util.debuglog; + + await engine.prepareOptions({}, testLogger); + + // After prepareOptions, util.debuglog should be patched + expect(util.debuglog).not.toBe(debuglogBeforeSetup); + + // Call cleanup + cleanupMonkeypatch(); + + // After cleanup, util.debuglog should be restored + expect(util.debuglog).toBe(debuglogBeforeSetup); + }); + }); }); diff --git a/src/engine/engine.ts b/src/engine/engine.ts index 57ada08..477d4a3 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -3,23 +3,31 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import {Schema} from '../deploy/schema'; -import {GHPages} from '../interfaces'; +import {GHPages, PublishOptions} from '../interfaces'; import {defaults} from './defaults'; - -import Git from 'gh-pages/lib/git'; +import { + PreparedOptions, + setupMonkeypatch, + mapNegatedBooleans, + handleUserCredentials, + warnDeprecatedParameters, + appendCIMetadata, + injectTokenIntoRepoUrl +} from './engine.prepare-options-helpers'; export async function run( dir: string, - options: Schema & { - dotfiles: boolean, - notfound: boolean, - nojekyll: boolean - }, + options: PreparedOptions, logger: logging.LoggerApi ) { options = await prepareOptions(options, logger); - // this has to occur _after_ the monkeypatch of util.debuglog: + // CRITICAL: Must require gh-pages AFTER monkeypatching util.debuglog + // gh-pages calls util.debuglog('gh-pages') during module initialization to set up its logger. + // If we require gh-pages before monkeypatching, it caches the original util.debuglog, + // and our monkeypatch won't capture the logging output. + // See prepareOptions() for the monkeypatch implementation. + // Note: Dynamic import required for monkeypatch timing (static import would cache before patch) const ghpages = require('gh-pages'); // always clean the cache directory. @@ -32,8 +40,7 @@ export async function run( await checkIfDistFolderExists(dir); await createNotFoundFile(dir, options, logger); - await createCnameFile(dir, options, logger); - await createNojekyllFile(dir, options, logger); + // Note: CNAME and .nojekyll files are now created by gh-pages v6+ via options await publishViaGhPages(ghpages, dir, options, logger); if (!options.dryRun) { @@ -43,150 +50,60 @@ export async function run( } } +/** + * Prepare and validate deployment options + * + * This orchestrator function: + * 1. Merges defaults with user options + * 2. Sets up monkeypatch for gh-pages logging + * 3. Maps negated boolean options (noDotfiles β†’ dotfiles) + * 4. Handles user credentials + * 5. Warns about deprecated parameters + * 6. Appends CI environment metadata + * 7. Discovers and injects remote URL with authentication tokens + * 8. Logs dry-run message if applicable + */ export async function prepareOptions( origOptions: Schema, logger: logging.LoggerApi -): Promise { - const options: Schema & { - dotfiles: boolean, - notfound: boolean, - nojekyll: boolean - } = { +): Promise { + // 1. Merge defaults with user options + const options: PreparedOptions = { ...defaults, ...origOptions }; - // this is the place where the old `noSilent` was enabled - // (which is now always enabled because gh-pages is NOT silent) - // monkeypatch util.debuglog to get all the extra information - // see https://stackoverflow.com/a/39129886 - const util = require('util'); - let debuglog = util.debuglog; - util.debuglog = set => { - if (set === 'gh-pages') { - return function () { - let message = util.format.apply(util, arguments); - logger.info(message); - }; - } - return debuglog(set); - }; + // 2. Setup monkeypatch for gh-pages logging (MUST be before requiring gh-pages) + setupMonkeypatch(logger); - // !! Important: Angular-CLI is NOT renaming the vars here !! - // so noDotfiles, noNotfound, and noNojekyll come in with no change - // we map this to dotfiles, notfound, nojekyll to have a consistent pattern - // between Commander and Angular-CLI - if (origOptions.noDotfiles) { - options.dotfiles = !origOptions.noDotfiles; - } - if (origOptions.noNotfound) { - options.notfound = !origOptions.noNotfound; - } - if (origOptions.noNojekyll) { - options.nojekyll = !origOptions.noNojekyll; - } + // 3. Map negated boolean options + mapNegatedBooleans(options, origOptions); - if (options.dryRun) { - logger.info('Dry-run: No changes are applied at all.'); - } - - if (options.name && options.email) { - options['user'] = { - name: options.name, - email: options.email - }; - } + // 4. Handle user credentials + handleUserCredentials(options, origOptions, logger); - if (process.env.TRAVIS) { - options.message += - ' -- ' + - process.env.TRAVIS_COMMIT_MESSAGE + - ' \n\n' + - 'Triggered by commit: https://github.com/' + - process.env.TRAVIS_REPO_SLUG + - '/commit/' + - process.env.TRAVIS_COMMIT + - '\n' + - 'Travis CI build: https://travis-ci.org/' + - process.env.TRAVIS_REPO_SLUG + - '/builds/' + - process.env.TRAVIS_BUILD_ID; - } + // 5. Warn about deprecated parameters + warnDeprecatedParameters(origOptions, logger); - if (process.env.CIRCLECI) { - options.message += - '\n\n' + - 'Triggered by commit: https://github.com/' + - process.env.CIRCLE_PROJECT_USERNAME + - '/' + - process.env.CIRCLE_PROJECT_REPONAME + - '/commit/' + - process.env.CIRCLE_SHA1 + - '\n' + - 'CircleCI build: ' + - process.env.CIRCLE_BUILD_URL; - } + // 6. Append CI environment metadata + appendCIMetadata(options); - if (process.env.GITHUB_ACTIONS) { - options.message += - '\n\n' + - 'Triggered by commit: https://github.com/' + - process.env.GITHUB_REPOSITORY + - '/commit/' + - process.env.GITHUB_SHA; - } + // 7. Discover and inject remote URL with authentication tokens + await injectTokenIntoRepoUrl(options); - // NEW in 0.6.2: always discover remote URL (if not set) - // this allows us to inject tokens from environment even if `--repo` is not set manually - if (!options.repo) { - options.repo = await getRemoteUrl(options); - } - - // for backwards compatibility only, - // in the past --repo=https://GH_TOKEN@github.com//.git was advised - // - // this repalcement was also used to inject other tokens into the URL, - // so it should only be removed with the next major version - if ( - process.env.GH_TOKEN && - options.repo && - options.repo.includes('GH_TOKEN') - ) { - options.repo = options.repo.replace('GH_TOKEN', process.env.GH_TOKEN); - } - // preffered way: token is replaced from plain URL - else if (options.repo && !options.repo.includes('x-access-token:')) { - if (process.env.GH_TOKEN) { - options.repo = options.repo.replace( - 'https://github.com/', - `https://x-access-token:${process.env.GH_TOKEN}@github.com/` - ); - } - - if (process.env.PERSONAL_TOKEN) { - options.repo = options.repo.replace( - 'https://github.com/', - `https://x-access-token:${process.env.PERSONAL_TOKEN}@github.com/` - ); - } - - if (process.env.GITHUB_TOKEN) { - options.repo = options.repo.replace( - 'https://github.com/', - `https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/` - ); - } + // 8. Log dry-run message if applicable + if (options.dryRun) { + logger.info('Dry-run: No changes are applied at all.'); } return options; } async function checkIfDistFolderExists(dir: string) { - if (await !fse.pathExists(dir)) { + // CRITICAL FIX: Operator precedence bug + // WRONG: await !fse.pathExists(dir) - applies ! to Promise (always false) + // RIGHT: !(await fse.pathExists(dir)) - awaits first, then negates boolean + if (!(await fse.pathExists(dir))) { throw new Error( 'Dist folder does not exist. Check the dir --dir parameter or build the project first!' ); @@ -220,77 +137,27 @@ async function createNotFoundFile( await fse.copy(indexHtml, notFoundFile); logger.info('404.html file created'); } catch (err) { + const message = err instanceof Error ? err.message : String(err); logger.info('index.html could not be copied to 404.html. Proceeding without it.'); - logger.debug('Diagnostic info: ' + err.message); + logger.debug('Diagnostic info: ' + message); return; } } -async function createCnameFile( - dir: string, - options: { - cname?: string, - dryRun?: boolean - }, - logger: logging.LoggerApi -) { - if (!options.cname) { - return; - } - - const cnameFile = path.join(dir, 'CNAME'); - if (options.dryRun) { - logger.info( - 'Dry-run / SKIPPED: creating of CNAME file with content: ' + options.cname - ); - return; - } - - try { - await fse.writeFile(cnameFile, options.cname); - logger.info('CNAME file created'); - } catch (err) { - throw new Error('CNAME file could not be created. ' + err.message); - } -} - -async function createNojekyllFile( - dir: string, - options: { - nojekyll: boolean, - dryRun?: boolean - }, - logger: logging.LoggerApi -) { - if (!options.nojekyll) { - return; - } - - const nojekyllFile = path.join(dir, '.nojekyll'); - if (options.dryRun) { - logger.info('Dry-run / SKIPPED: creating a .nojekyll file'); - return; - } - - try { - await fse.writeFile(nojekyllFile, ''); - logger.info('.nojekyll file created'); - } catch (err) { - throw new Error('.nojekyll file could not be created. ' + err.message); - } -} +// CNAME and .nojekyll files are now handled by gh-pages v6+ via the cname/nojekyll options +// Previously we created these files ourselves before publishing, but gh-pages PR #533 added native support async function publishViaGhPages( ghPages: GHPages, dir: string, - options: Schema & { - dotfiles: boolean, - notfound: boolean, - nojekyll: boolean - }, + options: PreparedOptions, logger: logging.LoggerApi ) { if (options.dryRun) { + // Note: options.repo may contain auth tokens. This is acceptable because: + // 1. CI environments (GitHub Actions, etc.) automatically mask secrets in logs + // 2. Dry-run is typically used for debugging, where seeing the full URL helps + // 3. Local dry-runs don't persist logs logger.info( `Dry-run / SKIPPED: publishing folder '${dir}' with the following options: ` + JSON.stringify( @@ -300,8 +167,8 @@ async function publishViaGhPages( remote: options.remote, message: options.message, branch: options.branch, - name: options.name ? `the name '${options.username} will be used for the commit` : 'local or global git user name will be used for the commit', - email: options.email ? `the email '${options.cname} will be used for the commit` : 'local or global git user email will be used for the commit', + name: options.name ? `the name '${options.name}' will be used for the commit` : 'local or global git user name will be used for the commit', + email: options.email ? `the email '${options.email}' will be used for the commit` : 'local or global git user email will be used for the commit', dotfiles: options.dotfiles ? `files starting with dot ('.') will be included` : `files starting with dot ('.') will be ignored`, notfound: options.notfound ? 'a 404.html file will be created' : 'a 404.html file will NOT be created', nojekyll: options.nojekyll ? 'a .nojekyll file will be created' : 'a .nojekyll file will NOT be created', @@ -317,20 +184,22 @@ async function publishViaGhPages( logger.info('πŸš€ Uploading via git, please wait...'); - // do NOT (!!) await ghPages.publish, - // the promise is implemented in such a way that it always succeeds – even on errors! - return new Promise((resolve, reject) => { - ghPages.publish(dir, options, error => { - if (error) { - return reject(error); - } - - resolve(undefined); - }); - }); -} + // Only pass options that gh-pages understands + // gh-pages v6+ supports cname and nojekyll options natively (PR #533) + const ghPagesOptions: PublishOptions = { + repo: options.repo, + branch: options.branch, + message: options.message, + remote: options.remote, + git: options.git as string | undefined, + add: options.add, + dotfiles: options.dotfiles, + user: options.user, + cname: options.cname, + nojekyll: options.nojekyll + }; -async function getRemoteUrl(options) { - const git = new Git(process.cwd(), options.git); - return await git.getRemoteUrl(options.remote); + // gh-pages v5+ fixed the Promise bug where errors didn't reject properly + // We can now safely await the promise directly + await ghPages.publish(dir, ghPagesOptions); } diff --git a/src/engine/gh-pages.d.ts b/src/engine/gh-pages.d.ts new file mode 100644 index 0000000..aa3828f --- /dev/null +++ b/src/engine/gh-pages.d.ts @@ -0,0 +1,13 @@ +/** + * Type declarations for gh-pages internal modules + * + * gh-pages/lib/git is an internal API we use to get the remote URL. + * See engine.prepare-options-helpers.ts for usage and upgrade risk documentation. + */ +declare module 'gh-pages/lib/git' { + class Git { + constructor(cwd: string, git?: string); + getRemoteUrl(remote?: string): Promise; + } + export = Git; +} diff --git a/src/interfaces.ts b/src/interfaces.ts index 1890b09..46c99d7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,14 +1,91 @@ +/** + * Angular outputPath configuration + * Can be either a string (path) or an object with base + browser properties + * See: https://angular.io/guide/workspace-config#output-path-configuration + */ +export interface AngularOutputPathObject { + base: string; + browser?: string; +} + +export type AngularOutputPath = string | AngularOutputPathObject; + +/** + * Type guard to check if outputPath is a valid object with base/browser properties. + * + * Validates: + * - value is an object (not null, not array) + * - base property exists and is a non-empty string + * - browser property, if present, is a string (can be empty for Angular 19+ SPA mode) + */ +export function isOutputPathObject(value: unknown): value is AngularOutputPathObject { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + + const obj = value as Record; + + // base must be a non-empty string + if (typeof obj.base !== 'string' || obj.base === '') { + return false; + } + + // browser, if present, must be a string (empty string is valid for SPA mode) + if ('browser' in obj && typeof obj.browser !== 'string') { + return false; + } + + return true; +} + +/** + * Git user credentials for commits + */ +export interface DeployUser { + name: string; + email: string; +} + +/** + * Options for gh-pages.publish() + * Based on https://github.com/tschaub/gh-pages#options + * + * Note: Only includes options that gh-pages actually accepts. + * Internal options (notfound, noDotfiles, noNotfound, noNojekyll, dryRun) + * are handled by angular-cli-ghpages before calling gh-pages. + */ +export interface PublishOptions { + repo?: string; + remote?: string; + branch?: string; + message?: string; + user?: { name: string; email: string }; + dotfiles?: boolean; + nojekyll?: boolean; + cname?: string; + add?: boolean; + git?: string; + [key: string]: unknown; // Allow additional gh-pages options +} + export interface GHPages { - publish(dir: string, options: any, callback: (error: any) => void); + // gh-pages v5+ supports both callback and Promise-based APIs + publish(dir: string, options: PublishOptions, callback: (error: Error | null) => void): void; + publish(dir: string, options: PublishOptions): Promise; clean?(): void; } +export interface ArchitectTarget { + builder: string; + options?: { + outputPath?: string | { base?: string; browser?: string }; + [key: string]: unknown; + }; +} + export interface WorkspaceProject { projectType?: string; - architect?: Record< - string, - { builder: string; options?: Record } - >; + architect?: Record; } export interface Workspace { @@ -17,5 +94,8 @@ export interface Workspace { export interface BuildTarget { name: string; - options?: Record; + options?: { + outputPath?: string | { base?: string; browser?: string }; + [key: string]: unknown; + }; } diff --git a/src/ng-add.spec.ts b/src/ng-add.spec.ts index 4c563aa..a625394 100644 --- a/src/ng-add.spec.ts +++ b/src/ng-add.spec.ts @@ -44,7 +44,7 @@ describe('ng-add', () => { it('should select the first project if there is only one', async () => { const tree = Tree.empty(); const angularJSON = generateAngularJson(); - delete (angularJSON as any).projects[PROJECT_NAME]; // delete one project so that one is left + delete angularJSON.projects[PROJECT_NAME]; // delete one project so that one is left tree.create('angular.json', JSON.stringify(angularJSON)); const resultTree = await ngAdd({ project: '' })( @@ -53,9 +53,8 @@ describe('ng-add', () => { ); const resultConfig = readJSONFromTree(resultTree, 'angular.json'); - expect( - resultConfig.projects[OTHER_PROJECT_NAME].architect.deploy - ).toBeTruthy(); + const deployTarget = resultConfig.projects[OTHER_PROJECT_NAME].architect.deploy; + expect(deployTarget.builder).toBe('angular-cli-ghpages:deploy'); }); }); @@ -117,14 +116,18 @@ describe('ng-add', () => { ); }); - it('should throw if app does not have architect configured', async () => { + it('should throw if app does not have build target configured', async () => { const tree = Tree.empty(); tree.create( 'angular.json', JSON.stringify({ version: 1, projects: { - [PROJECT_NAME]: { projectType: 'application', root: PROJECT_NAME } + [PROJECT_NAME]: { + projectType: 'application', + root: PROJECT_NAME, + architect: {} + } } }) ); @@ -134,7 +137,135 @@ describe('ng-add', () => { project: PROJECT_NAME })(tree, {} as SchematicContext) ).rejects.toThrowError( - 'Cannot read the output path (architect.build.options.outputPath) of the Angular project "THEPROJECT" in angular.json' + /Cannot find build target for the Angular project/ + ); + }); + }); + + describe('Angular 17+ outputPath formats', () => { + it('should accept Angular 20+ projects without outputPath (uses default)', async () => { + const tree = Tree.empty(); + tree.create( + 'angular.json', + JSON.stringify({ + version: 1, + projects: { + [PROJECT_NAME]: { + projectType: 'application', + root: PROJECT_ROOT, + architect: { + build: { + builder: '@angular/build:application', + options: { + // Angular 20+ omits outputPath - uses sensible default + } + } + } + } + } + }) + ); + + const result = await ngAdd({ project: PROJECT_NAME })( + tree, + {} as SchematicContext + ); + + const resultConfig = readJSONFromTree(result, 'angular.json'); + const deployTarget = resultConfig.projects[PROJECT_NAME].architect.deploy; + expect(deployTarget.builder).toBe('angular-cli-ghpages:deploy'); + }); + + it('should accept outputPath as object with base property', async () => { + const tree = Tree.empty(); + tree.create( + 'angular.json', + JSON.stringify({ + version: 1, + projects: { + [PROJECT_NAME]: { + projectType: 'application', + root: PROJECT_ROOT, + architect: { + build: { + options: { + outputPath: { base: 'dist/my-app', browser: '' } + } + } + } + } + } + }) + ); + + const result = await ngAdd({ project: PROJECT_NAME })( + tree, + {} as SchematicContext + ); + + const resultConfig = readJSONFromTree(result, 'angular.json'); + const deployTarget = resultConfig.projects[PROJECT_NAME].architect.deploy; + expect(deployTarget.builder).toBe('angular-cli-ghpages:deploy'); + }); + + it('should accept outputPath object with base and browser properties', async () => { + const tree = Tree.empty(); + tree.create( + 'angular.json', + JSON.stringify({ + version: 1, + projects: { + [PROJECT_NAME]: { + projectType: 'application', + root: PROJECT_ROOT, + architect: { + build: { + options: { + outputPath: { base: 'dist/app', browser: 'browser', server: 'server' } + } + } + } + } + } + }) + ); + + const result = await ngAdd({ project: PROJECT_NAME })( + tree, + {} as SchematicContext + ); + + const resultConfig = readJSONFromTree(result, 'angular.json'); + const deployTarget = resultConfig.projects[PROJECT_NAME].architect.deploy; + expect(deployTarget.builder).toBe('angular-cli-ghpages:deploy'); + }); + + it('should reject invalid outputPath object without base property', async () => { + const tree = Tree.empty(); + tree.create( + 'angular.json', + JSON.stringify({ + version: 1, + projects: { + [PROJECT_NAME]: { + projectType: 'application', + root: PROJECT_ROOT, + architect: { + build: { + options: { + outputPath: { browser: 'browser' } // missing base - invalid + } + } + } + } + } + }) + ); + + await expect( + ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext) + ).rejects.toThrowError( + /Invalid outputPath configuration.*Expected undefined.*a string.*an object with a "base" property/ ); }); }); @@ -148,7 +279,26 @@ function readJSONFromTree(tree: Tree, file: string) { return JSON.parse(tree.read(file)!.toString()); } -function generateAngularJson() { +interface AngularJsonProject { + projectType: string; + root: string; + architect: { + build: { + options: { + outputPath: string; + }; + }; + }; +} + +interface AngularJson { + version: number; + projects: { + [key: string]: AngularJsonProject; + }; +} + +function generateAngularJson(): AngularJson { return { version: 1, projects: { diff --git a/src/ng-add.ts b/src/ng-add.ts index 670511b..fc6fd31 100644 --- a/src/ng-add.ts +++ b/src/ng-add.ts @@ -41,9 +41,29 @@ export const ngAdd = (options: NgAddOptions) => async ( ); } - if (!project.targets.get('build')?.options?.outputPath) { + // Validate build target exists (required for deployment) + const buildTarget = project.targets.get('build'); + if (!buildTarget) { throw new SchematicsException( - `Cannot read the output path (architect.build.options.outputPath) of the Angular project "${options.project}" in angular.json` + `Cannot find build target for the Angular project "${options.project}" in angular.json.` + ); + } + + // outputPath validation: + // - Angular 20+: outputPath is omitted (uses default dist/) + // - Angular 17-19: object format { base: "dist/app", browser: "", ... } + // - Earlier versions: string format "dist/app" + // See: https://github.com/angular/angular-cli/pull/26675 + const outputPath = buildTarget.options?.outputPath; + const hasValidOutputPath = + outputPath === undefined || // Angular 20+ uses sensible defaults + typeof outputPath === 'string' || + (typeof outputPath === 'object' && outputPath !== null && 'base' in outputPath); + + if (!hasValidOutputPath) { + throw new SchematicsException( + `Invalid outputPath configuration for the Angular project "${options.project}" in angular.json. ` + + `Expected undefined (default), a string, or an object with a "base" property.` ); } diff --git a/src/package-lock.json b/src/package-lock.json index 09c997a..1d650ad 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,20 +1,19 @@ { "name": "angular-cli-ghpages", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "angular-cli-ghpages", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { - "@angular-devkit/architect": "~0.1800.0", - "@angular-devkit/core": "^18.0.0", - "@angular-devkit/schematics": "^18.0.0", - "commander": "^3.0.0-0", + "@angular-devkit/architect": ">=0.1800.0", + "@angular-devkit/core": ">=18.0.0", + "@angular-devkit/schematics": ">=18.0.0", "fs-extra": "^11.2.0", - "gh-pages": "^3.1.0" + "gh-pages": "6.3.0" }, "bin": { "angular-cli-ghpages": "angular-cli-ghpages", @@ -33,6 +32,222 @@ "rimraf": "^5.0.5", "ts-jest": "^29.1.2", "typescript": "~5.2.2" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "@angular/cli": ">=18.0.0" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.6.1.tgz", + "integrity": "sha512-wV/gNRkzb7sI9vs1OneG129hwe3Q5zPj7zigz3Ps7M5Lpo2hSorrOnXNodHEOV+yXE/ks4Pd+G3CDFIjFTWhMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.40.1.tgz", + "integrity": "sha512-cxKNATPY5t+Mv8XAVTI57altkaPH+DZi4uMrnexPxPHODMljhGYY+GDZyHwv9a+8CbZHcY372OkxXrDMZA4Lnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.40.1.tgz", + "integrity": "sha512-XP008aMffJCRGAY8/70t+hyEyvqqV7YKm502VPu0+Ji30oefrTn2al7LXkITz7CK6I4eYXWRhN6NaIUi65F1OA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.40.1.tgz", + "integrity": "sha512-gWfQuQUBtzUboJv/apVGZMoxSaB0M4Imwl1c9Ap+HpCW7V0KhjBddqF2QQt5tJZCOFsfNIgBbZDGsEPaeKUosw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.40.1.tgz", + "integrity": "sha512-RTLjST/t+lsLMouQ4zeLJq2Ss+UNkLGyNVu+yWHanx6kQ3LT5jv8UvPwyht9s7R6jCPnlSI77WnL80J32ZuyJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.40.1.tgz", + "integrity": "sha512-2FEK6bUomBzEYkTKzD0iRs7Ljtjb45rKK/VSkyHqeJnG+77qx557IeSO0qVFE3SfzapNcoytTofnZum0BQ6r3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.40.1.tgz", + "integrity": "sha512-Nju4NtxAvXjrV2hHZNLKVJLXjOlW6jAXHef/CwNzk1b2qIrCWDO589ELi5ZHH1uiWYoYyBXDQTtHmhaOVVoyXg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.1.tgz", + "integrity": "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.40.1.tgz", + "integrity": "sha512-z+BPlhs45VURKJIxsR99NNBWpUEEqIgwt10v/fATlNxc4UlXvALdOsWzaFfe89/lbP5Bu4+mbO59nqBC87ZM/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.40.1.tgz", + "integrity": "sha512-VJMUMbO0wD8Rd2VVV/nlFtLJsOAQvjnVNGkMkspFiFhpBA7s/xJOb+fJvvqwKFUjbKTUA7DjiSi1ljSMYBasXg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.40.1.tgz", + "integrity": "sha512-ehvJLadKVwTp9Scg9NfzVSlBKH34KoWOQNTaN8i1Ac64AnO6iH2apJVSP6GOxssaghZ/s8mFQsDH3QIZoluFHA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.40.1.tgz", + "integrity": "sha512-PbidVsPurUSQIr6X9/7s34mgOMdJnn0i6p+N6Ab+lsNhY5eiu+S33kZEpZwkITYBCIbhzDLOvb7xZD3gDi+USA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.40.1.tgz", + "integrity": "sha512-ThZ5j6uOZCF11fMw9IBkhigjOYdXGXQpj6h4k+T9UkZrF2RlKcPynFzDeRgaLdpYk8Yn3/MnFbwUmib7yxj5Lw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.40.1.tgz", + "integrity": "sha512-H1gYPojO6krWHnUXu/T44DrEun/Wl95PJzMXRcM/szstNQczSbwq6wIFJPI9nyE95tarZfUNU3rgorT+wZ6iCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@ampproject/remapping": { @@ -120,434 +335,801 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "node_modules/@angular/cli": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.4.tgz", + "integrity": "sha512-L4uKhC3KorF04x9A7noff2m25Phkq54wdqzuWNnbGg3bNfOHdXMv97t2e02J1mk+XOeEcPfDJmOiXj4fcviCLA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@angular-devkit/architect": "0.2100.4", + "@angular-devkit/core": "21.0.4", + "@angular-devkit/schematics": "21.0.4", + "@inquirer/prompts": "7.9.0", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.24.0", + "@schematics/angular": "21.0.4", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.40.1", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.1", + "pacote": "21.0.3", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", + "yargs": "18.0.0", + "zod": "4.1.13" + }, + "bin": { + "ng": "bin/ng.js" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.2100.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.4.tgz", + "integrity": "sha512-tKtb0I8AU59m75JjHlL1XEsoPxVaEWhnHKeesDpk49RNm0sVqWnfXesse8IXqdVds0Hpjisc3In7j4xKbigfXg==", + "license": "MIT", + "peer": true, "dependencies": { - "color-convert": "^1.9.0" + "@angular-devkit/core": "21.0.4", + "rxjs": "7.8.2" }, "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.4.tgz", + "integrity": "sha512-Mbze8tMtBs7keSOx4UIR9utLQs1uSiGjfTaOkCu/dbBEiG6umopy1OlUCvHiHyeiYqh+wR0yiGtTS+Cexo5iLg==", + "license": "MIT", + "peer": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.4.tgz", + "integrity": "sha512-am39kuaBB/v7RL++bsepvUhP2JKDmfMLQbyJvyHIG6UxnQztxQYZ2/CiPb91dz9NMiqAZqIJaN+kqvIc8h7AeQ==", + "license": "MIT", + "peer": true, "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "@angular-devkit/core": "21.0.4", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.19", + "ora": "9.0.0", + "rxjs": "7.8.2" + }, "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, "dependencies": { - "has-flag": "^3.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=4" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, + "node_modules/@angular/cli/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, + "node_modules/@angular/cli/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, + "node_modules/@angular/cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, + "node_modules/@angular/cli/node_modules/cli-spinners": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, + "node_modules/@angular/cli/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "peer": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=20" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, + "node_modules/@angular/cli/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/types": "^7.22.5" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" + "node_modules/@angular/cli/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "peer": true + }, + "node_modules/@angular/cli/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@angular/cli/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, + "node_modules/@angular/cli/node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@angular/cli/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, + "node_modules/@angular/cli/node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/types": "^7.22.5" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, + "node_modules/@angular/cli/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/cli/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/types": "^7.22.5" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=6.9.0" + "node": ">=10" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, + "node_modules/@angular/cli/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, + "node_modules/@angular/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">= 12" } }, - "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, + "node_modules/@angular/cli/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "node_modules/@angular/cli/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "peer": true, "dependencies": { - "color-convert": "^1.9.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, + "node_modules/@angular/cli/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "node_modules/@angular/cli/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "peer": true, "dependencies": { - "color-name": "1.1.3" + "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" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, + "node_modules/@angular/cli/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "peer": true, "engines": { - "node": ">=0.8.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "node_modules/@angular/cli/node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, @@ -688,14 +1270,15 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -723,14 +1306,14 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -761,10 +1344,11 @@ "dev": true }, "node_modules/@bcherny/json-schema-ref-parser/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -778,467 +1362,1829 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "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" - }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "license": "MIT", + "peer": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", + "peer": true, "dependencies": { - "ansi-regex": "^6.0.1" + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, "engines": { - "node": ">=12" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "peer": true, "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" }, "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "@types/node": ">=18" }, "peerDependenciesMeta": { - "node-notifier": { + "@types/node": { "optional": true } } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "license": "MIT", + "peer": true, "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", + "peer": true, "dependencies": { - "jest-get-type": "^29.6.3" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, + "node_modules/@inquirer/prompts": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", + "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "@inquirer/checkbox": "^4.3.0", + "@inquirer/confirm": "^5.1.19", + "@inquirer/editor": "^4.2.21", + "@inquirer/expand": "^4.0.21", + "@inquirer/input": "^4.2.5", + "@inquirer/number": "^3.0.21", + "@inquirer/password": "^4.0.21", + "@inquirer/rawlist": "^4.1.9", + "@inquirer/search": "^3.2.0", + "@inquirer/select": "^4.4.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "license": "MIT", + "peer": true, "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz", + "integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "license": "ISC", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", + "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", + "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "license": "ISC", + "peer": true, + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", + "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@schematics/angular": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.4.tgz", + "integrity": "sha512-/jJOf3iLvTaVa25xwiYLsfmidVAzC6rPy3Nl85iRo5bVod8be+KhHTn8aGq/8o7pzzB6Cin1oLs+riPR1nLVhg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@angular-devkit/core": "21.0.4", + "@angular-devkit/schematics": "21.0.4", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.4.tgz", + "integrity": "sha512-Mbze8tMtBs7keSOx4UIR9utLQs1uSiGjfTaOkCu/dbBEiG6umopy1OlUCvHiHyeiYqh+wR0yiGtTS+Cexo5iLg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.4.tgz", + "integrity": "sha512-am39kuaBB/v7RL++bsepvUhP2JKDmfMLQbyJvyHIG6UxnQztxQYZ2/CiPb91dz9NMiqAZqIJaN+kqvIc8h7AeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@angular-devkit/core": "21.0.4", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.19", + "ora": "9.0.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@schematics/angular/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@schematics/angular/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@schematics/angular/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "peer": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@schematics/angular/node_modules/cli-spinners": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@schematics/angular/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "peer": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@schematics/angular/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, + "node_modules/@schematics/angular/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@schematics/angular/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "peer": true, "dependencies": { - "@sinclair/typebox": "^0.27.8" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, + "node_modules/@schematics/angular/node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@schematics/angular/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-function": "^5.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, + "node_modules/@schematics/angular/node_modules/ora": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, + "node_modules/@schematics/angular/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@schematics/angular/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "tslib": "^2.1.0" + } + }, + "node_modules/@schematics/angular/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@schematics/angular/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "peer": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 12" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, + "node_modules/@schematics/angular/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "peer": true, "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, + "node_modules/@schematics/angular/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, "engines": { - "node": ">=6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, + "node_modules/@sigstore/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", + "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dev": true, + "node_modules/@sigstore/sign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", + "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, + "node_modules/@sigstore/tuf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", + "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, "engines": { - "node": ">=14" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sinclair/typebox": { @@ -1265,6 +3211,46 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1400,7 +3386,7 @@ "version": "20.11.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.7.tgz", "integrity": "sha512-GPmeN1C3XAyV5uybAf4cMLWT9fDWcmQhZVtMFu7OR32WjrqGG+Wnk2V1d0bmtUyE/Zy1QJ9BxyiTih9z8Oks8A==", - "dev": true, + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1432,11 +3418,51 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -1452,7 +3478,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, "dependencies": { "ajv": "^8.0.0" }, @@ -1465,6 +3490,32 @@ } } }, + "node_modules/algoliasearch": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.1.tgz", + "integrity": "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/abtesting": "1.6.1", + "@algolia/client-abtesting": "5.40.1", + "@algolia/client-analytics": "5.40.1", + "@algolia/client-common": "5.40.1", + "@algolia/client-insights": "5.40.1", + "@algolia/client-personalization": "5.40.1", + "@algolia/client-query-suggestions": "5.40.1", + "@algolia/client-search": "5.40.1", + "@algolia/ingestion": "1.40.1", + "@algolia/monitoring": "1.40.1", + "@algolia/recommend": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1484,7 +3535,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1493,7 +3543,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1533,31 +3582,19 @@ } }, "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dependencies": { - "lodash": "^4.17.14" - } + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/babel-jest": { "version": "29.7.0", @@ -1669,7 +3706,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -1745,22 +3783,49 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1849,6 +3914,144 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", @@ -1918,6 +4121,23 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT", + "peer": true + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1979,6 +4199,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "license": "MIT", + "peer": true, + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -2019,7 +4312,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2030,13 +4322,23 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT", + "peer": true }, "node_modules/commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/commondir": { "version": "1.0.1", @@ -2046,7 +4348,32 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -2054,6 +4381,26 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -2079,6 +4426,20 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2101,10 +4462,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2125,12 +4486,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2173,6 +4534,16 @@ "clone": "^1.0.2" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2191,11 +4562,46 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.4.647", @@ -2204,9 +4610,10 @@ "dev": true }, "node_modules/email-addresses": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", - "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "license": "MIT" }, "node_modules/emittery": { "version": "0.13.1", @@ -2220,11 +4627,88 @@ "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "peer": true }, "node_modules/error-ex": { "version": "1.3.2", @@ -2235,15 +4719,50 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es5-ext": { - "version": "0.10.61", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz", - "integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, + "license": "ISC", "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -2287,11 +4806,17 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -2301,6 +4826,29 @@ "node": ">=8" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2314,6 +4862,16 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -2324,6 +4882,36 @@ "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT", + "peer": true + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2372,6 +4960,73 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -2390,8 +5045,23 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -2399,6 +5069,32 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2433,10 +5129,10 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2444,6 +5140,28 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -2500,6 +5218,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -2513,10 +5251,24 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -2536,7 +5288,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2554,11 +5306,48 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2568,6 +5357,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -2593,17 +5396,18 @@ } }, "node_modules/gh-pages": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-3.2.3.tgz", - "integrity": "sha512-jA1PbapQ1jqzacECfjUaO9gV8uBgU6XNMV0oXLtfCX3haGLe5Atq8BxlrADhbD6/UdG9j6tZLWAkAybndOXTJg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "license": "MIT", "dependencies": { - "async": "^2.6.1", - "commander": "^2.18.0", - "email-addresses": "^3.0.1", + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", "filenamify": "^4.3.0", "find-cache-dir": "^3.3.1", - "fs-extra": "^8.1.0", - "globby": "^6.1.0" + "fs-extra": "^11.1.1", + "globby": "^11.1.0" }, "bin": { "gh-pages": "bin/gh-pages.js", @@ -2613,44 +5417,11 @@ "node": ">=10" } }, - "node_modules/gh-pages/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/gh-pages/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/gh-pages/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/gh-pages/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2666,6 +5437,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-promise": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz", @@ -2695,18 +5478,36 @@ } }, "node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -2723,24 +5524,116 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "license": "ISC", + "peer": true, + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2750,6 +5643,23 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2770,6 +5680,44 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "license": "ISC", + "peer": true, + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -2793,7 +5741,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -2802,6 +5749,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2812,6 +5760,36 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2819,12 +5797,15 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2834,7 +5815,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2843,7 +5823,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -2861,7 +5840,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2882,7 +5860,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2926,8 +5904,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -3086,16 +6063,14 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -3777,17 +6752,29 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3860,8 +6847,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json5": { "version": "2.2.3", @@ -3892,6 +6878,16 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT", + "peer": true + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3916,6 +6912,109 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "peer": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3930,7 +7029,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -3954,6 +7054,189 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "peer": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "peer": true + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "peer": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3995,12 +7278,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -4010,6 +7339,26 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -4026,25 +7375,74 @@ "timers-ext": "^0.1.7" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4054,10 +7452,24 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4072,14 +7484,157 @@ "dev": true }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-fetch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", + "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4093,10 +7648,20 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/mz": { "version": "2.7.0", @@ -4115,41 +7680,312 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, + "node_modules/node-gyp": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.1.0.tgz", + "integrity": "sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.2", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "license": "ISC", + "peer": true, + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "license": "ISC", + "peer": true, + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", + "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", + "license": "ISC", + "peer": true, + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", + "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "license": "ISC", + "peer": true, + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "dev": true, + "node_modules/npm-pick-manifest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "license": "ISC", + "peer": true, "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-run-path": { @@ -4172,6 +8008,32 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4243,6 +8105,19 @@ "node": ">=8" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -4251,6 +8126,45 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", + "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^12.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4269,6 +8183,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4281,6 +8246,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -4289,7 +8255,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4297,45 +8262,63 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { - "node": "14 || >=16.14" + "node": ">=8" } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -4343,33 +8326,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -4379,6 +8335,16 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -4416,12 +8382,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4435,11 +8425,24 @@ "node": ">= 6" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4460,6 +8463,68 @@ } ] }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -4491,24 +8556,26 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4556,6 +8623,33 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT", + "peer": true + }, "node_modules/rimraf": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", @@ -4575,41 +8669,42 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4620,6 +8715,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -4635,6 +8777,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4643,11 +8792,64 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4659,17 +8861,110 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sigstore": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -4680,11 +8975,97 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -4713,12 +9094,61 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0", + "peer": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -4731,6 +9161,29 @@ "node": ">=10" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -4754,7 +9207,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4770,6 +9222,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4783,7 +9236,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4797,6 +9249,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4869,7 +9322,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4877,6 +9329,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4962,26 +9441,65 @@ "next-tick": "1" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4989,6 +9507,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -5096,8 +9624,22 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/type": { "version": "1.2.0", @@ -5126,6 +9668,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -5143,7 +9700,33 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "devOptional": true + }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "license": "ISC", + "peer": true, + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/universalify": { "version": "2.0.0", @@ -5153,6 +9736,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -5196,7 +9789,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -5221,6 +9813,37 @@ "node": ">=10.12.0" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -5243,7 +9866,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5277,6 +9899,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5320,7 +9943,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -5369,6 +9991,52 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/src/package.json b/src/package.json index cf04f26..4b5ee8d 100644 --- a/src/package.json +++ b/src/package.json @@ -1,8 +1,13 @@ { "name": "angular-cli-ghpages", - "version": "2.0.3", + "version": "3.0.0", "description": "Deploy your Angular app to GitHub Pages or Cloudflare Pages directly from the Angular CLI (ng deploy)", "main": "index.js", + "types": "index.d.ts", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, "bin": { "angular-cli-ghpages": "angular-cli-ghpages", "ngh": "angular-cli-ghpages" @@ -10,10 +15,8 @@ "scripts": { "prebuild": "rimraf dist && json2ts deploy/schema.json > deploy/schema.d.ts", "build": "tsc -p tsconfig.build.json", - "postbuild": "copyfiles builders.json collection.json ng-add-schema.json package.json angular-cli-ghpages deploy/schema.json dist && copyfiles ../README.md dist/README.md", - "test": "jest", - "prepublishOnly": "echo \"*** MAKE SURE TO TYPE 'npm run publish-to-npm' AND NOT 'npm publish'! ***\"", - "publish-to-npm": "npm run build && cd dist && npm publish" + "postbuild": "copyfiles builders.json collection.json ng-add-schema.json package.json angular-cli-ghpages deploy/schema.json dist && copyfiles ../README.md dist/README.md && copyfiles commander-fork/index.js dist", + "test": "jest" }, "schematics": "./collection.json", "builders": "./builders.json", @@ -42,7 +45,7 @@ "angular-cli-github-pages" ], "author": { - "name": "Johannes Hoppe", + "name": "Angular.Schule (by Johannes Hoppe)", "email": "johannes.hoppe@haushoppe-its.de" }, "contributors": [ @@ -69,11 +72,13 @@ "typescript": "~5.2.2" }, "dependencies": { - "@angular-devkit/architect": "~0.1800.0", - "@angular-devkit/core": "^18.0.0", - "@angular-devkit/schematics": "^18.0.0", - "commander": "^3.0.0-0", + "@angular-devkit/architect": ">=0.1800.0 <0.2200.0", + "@angular-devkit/core": ">=18.0.0 <22.0.0", + "@angular-devkit/schematics": ">=18.0.0 <22.0.0", "fs-extra": "^11.2.0", - "gh-pages": "^3.1.0" + "gh-pages": "6.3.0" + }, + "peerDependencies": { + "@angular/cli": ">=18.0.0 <22.0.0" } } diff --git a/src/parameter-tests/build-target.spec.ts b/src/parameter-tests/build-target.spec.ts new file mode 100644 index 0000000..b8dce1b --- /dev/null +++ b/src/parameter-tests/build-target.spec.ts @@ -0,0 +1,275 @@ +import { BuilderContext, BuilderRun, ScheduleOptions, Target } from '@angular-devkit/architect/src'; +import { JsonObject, logging } from '@angular-devkit/core'; +import deploy from '../deploy/actions'; +import { BuildTarget } from '../interfaces'; +import { Schema } from '../deploy/schema'; + +/** + * BUILD TARGET RESOLUTION TESTS + * + * Tests for Angular Builder-specific target parameters: + * - buildTarget: Standard build target + * - prerenderTarget: SSG/prerender build target + * - noBuild: Skip build process + * + * These tests verify the builder correctly resolves which build target + * to use and whether to trigger a build at all. + */ + +describe('Build Target Resolution', () => { + let context: BuilderContext; + let scheduleTargetSpy: jest.SpyInstance; + let capturedOptions: Schema | null = null; + + const PROJECT = 'test-project'; + + const mockEngine = { + run: (dir: string, options: Schema, logger: logging.LoggerApi) => { + capturedOptions = options; + return Promise.resolve(); + } + }; + + beforeEach(() => { + capturedOptions = null; + context = createMockContext(); + scheduleTargetSpy = jest.spyOn(context, 'scheduleTarget'); + }); + + describe('buildTarget parameter', () => { + it('should use buildTarget when specified', async () => { + const buildTarget = `${PROJECT}:build:staging`; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'build', + configuration: 'staging' + }, + {} + ); + }); + + it('should parse buildTarget with project:target:configuration format', async () => { + const buildTarget = 'myapp:build:production'; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: 'myapp', + target: 'build', + configuration: 'production' + }, + {} + ); + }); + + it('should handle buildTarget without configuration', async () => { + const buildTarget = `${PROJECT}:build`; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'build', + configuration: undefined + }, + {} + ); + }); + + it('should default to project:build:production when buildTarget not specified', async () => { + const defaultTarget = `${PROJECT}:build:production`; + const expectedBuildTarget: BuildTarget = { name: defaultTarget }; + const options: Schema = { noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'build', + configuration: 'production' + }, + {} + ); + }); + }); + + describe('prerenderTarget parameter', () => { + it('should use prerenderTarget for SSG builds', async () => { + const prerenderTarget = `${PROJECT}:prerender:production`; + const expectedBuildTarget: BuildTarget = { name: prerenderTarget }; + const options: Schema = { prerenderTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'prerender', + configuration: 'production' + }, + {} + ); + }); + + it('should parse prerenderTarget with project:target:configuration format', async () => { + const prerenderTarget = 'ssg-app:prerender:staging'; + const expectedBuildTarget: BuildTarget = { name: prerenderTarget }; + const options: Schema = { prerenderTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: 'ssg-app', + target: 'prerender', + configuration: 'staging' + }, + {} + ); + }); + + it('should prefer prerenderTarget over buildTarget when both specified', async () => { + // Per builder.ts lines 45-47: prerenderTarget takes precedence + const prerenderTarget = `${PROJECT}:prerender:production`; + const buildTarget = `${PROJECT}:build:production`; + const expectedPrerenderTarget: BuildTarget = { name: prerenderTarget }; + const options: Schema = { prerenderTarget, buildTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedPrerenderTarget, options); + + // Should use prerenderTarget (explicit precedence in code) + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'prerender', + configuration: 'production' + }, + {} + ); + }); + + }); + + describe('noBuild parameter', () => { + it('should skip build when noBuild is true', async () => { + const buildTarget = `${PROJECT}:build:production`; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget, noBuild: true }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + // Should NOT call scheduleTarget when noBuild is true + expect(scheduleTargetSpy).not.toHaveBeenCalled(); + }); + + it('should trigger build when noBuild is false', async () => { + const buildTarget = `${PROJECT}:build:production`; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget, noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + // Should call scheduleTarget when noBuild is false + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + }); + + it('should default to building when noBuild is not specified', async () => { + const buildTarget = `${PROJECT}:build:production`; + const expectedBuildTarget: BuildTarget = { name: buildTarget }; + const options: Schema = { buildTarget }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + // Default is noBuild: false (should build) + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + }); + + it('should skip build with noBuild=true even when prerenderTarget specified', async () => { + const prerenderTarget = `${PROJECT}:prerender:production`; + const expectedPrerenderTarget: BuildTarget = { name: prerenderTarget }; + const options: Schema = { prerenderTarget, noBuild: true }; + + await deploy(mockEngine, context, expectedPrerenderTarget, options); + + // Should NOT build even with prerenderTarget + expect(scheduleTargetSpy).not.toHaveBeenCalled(); + }); + + it('should trigger build with noBuild=false when default target used', async () => { + const defaultTarget = `${PROJECT}:build:production`; + const expectedBuildTarget: BuildTarget = { name: defaultTarget }; + const options: Schema = { noBuild: false }; + + await deploy(mockEngine, context, expectedBuildTarget, options); + + // Should build with default target + expect(scheduleTargetSpy).toHaveBeenCalledTimes(1); + expect(scheduleTargetSpy).toHaveBeenCalledWith( + { + project: PROJECT, + target: 'build', + configuration: 'production' + }, + {} + ); + }); + }); +}); + +function createMockContext(): BuilderContext { + const mockContext: Partial = { + target: { + configuration: 'production', + project: 'test-project', + target: 'deploy' + }, + builder: { + builderName: 'angular-cli-ghpages:deploy', + description: 'Deploy to GitHub Pages', + optionSchema: false + }, + currentDirectory: '/test', + id: 1, + logger: new logging.NullLogger(), + workspaceRoot: '/test', + addTeardown: _ => {}, + validateOptions: (_options: JsonObject, _builderName: string) => Promise.resolve({} as T), + getBuilderNameForTarget: () => Promise.resolve(''), + getTargetOptions: (_: Target) => + Promise.resolve({ + outputPath: 'dist/test-project' + } as JsonObject), + reportProgress: (_: number, __?: number, ___?: string) => {}, + reportStatus: (_: string) => {}, + reportRunning: () => {}, + scheduleBuilder: (_: string, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({} as BuilderRun), + scheduleTarget: (_: Target, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({ + result: Promise.resolve({ + success: true, + error: '', + info: {}, + target: {} as Target + }) + } as BuilderRun) + }; + + return mockContext as BuilderContext; +} diff --git a/src/parameter-tests/builder-integration.spec.ts b/src/parameter-tests/builder-integration.spec.ts new file mode 100644 index 0000000..558f0af --- /dev/null +++ b/src/parameter-tests/builder-integration.spec.ts @@ -0,0 +1,399 @@ +import { BuilderContext, BuilderRun, ScheduleOptions, Target } from '@angular-devkit/architect/src'; +import { JsonObject, logging } from '@angular-devkit/core'; +import { BuildTarget, PublishOptions } from '../interfaces'; +import { Schema } from '../deploy/schema'; +import { cleanupMonkeypatch } from '../engine/engine.prepare-options-helpers'; + +/** + * ANGULAR BUILDER INTEGRATION TESTS + * + * These tests verify the COMPLETE Angular Builder execution path: + * angular.json config + * β†’ deploy/actions.ts (builder entry point) + * β†’ engine.run() (REAL engine, not mocked) + * β†’ engine.prepareOptions() (parameter transformation) + * β†’ gh-pages.publish() (MOCKED to capture final options) + * + * This ensures that parameter transformation from Angular Builder format + * (noDotfiles, noNotfound, noNojekyll) to engine format (dotfiles, notfound, nojekyll) + * works correctly in the complete execution flow. + * + * WHAT'S REAL vs MOCKED: + * βœ… REAL: deploy/actions.ts, engine/engine.ts, prepareOptions() + * ❌ MOCKED: gh-pages.publish() (to capture final options), fs-extra, gh-pages/lib/git + * This IS a true integration test - we test the full internal code path with external dependencies mocked. + */ + +// Captured options from gh-pages.publish() +let capturedPublishOptions: PublishOptions | null = null; + +// Mock gh-pages/lib/git module (imported by engine.ts) +jest.mock('gh-pages/lib/git', () => { + return jest.fn().mockImplementation(() => ({ + // Mock git instance methods if needed + })); +}); + +// Mock gh-pages module - gh-pages v5+ uses Promise-based API +jest.mock('gh-pages', () => ({ + clean: jest.fn(), + publish: jest.fn((dir: string, options: PublishOptions) => { + capturedPublishOptions = options; + return Promise.resolve(); + }) +})); + +// Mock fs module +jest.mock('fs', () => ({ + existsSync: jest.fn(() => true), + writeFileSync: jest.fn(), + readFileSync: jest.fn(() => ''), + mkdirSync: jest.fn(), + readdirSync: jest.fn(() => []), + statSync: jest.fn(() => ({ isDirectory: () => false })), + promises: { + readFile: jest.fn(() => Promise.resolve('')), + writeFile: jest.fn(() => Promise.resolve()), + }, + native: {} // For fs-extra +})); + +// Mock fs-extra module (it extends fs) +jest.mock('fs-extra', () => ({ + existsSync: jest.fn(() => true), + writeFileSync: jest.fn(), + writeFile: jest.fn(() => Promise.resolve()), + readFileSync: jest.fn(() => ''), + readFile: jest.fn(() => Promise.resolve('')), + mkdirSync: jest.fn(), + readdirSync: jest.fn(() => []), + statSync: jest.fn(() => ({ isDirectory: () => false })), + promises: { + readFile: jest.fn(() => Promise.resolve('')), + writeFile: jest.fn(() => Promise.resolve()), + }, + native: {}, + ensureDirSync: jest.fn(), + emptyDirSync: jest.fn(), + copy: jest.fn(() => Promise.resolve()), + copySync: jest.fn(), + removeSync: jest.fn(), + pathExists: jest.fn(() => Promise.resolve(true)), + pathExistsSync: jest.fn(() => true), +})); + +// Import after mocking +import deploy from '../deploy/actions'; +import * as engine from '../engine/engine'; + +describe('Angular Builder Integration Tests', () => { + let context: BuilderContext; + let originalEnv: NodeJS.ProcessEnv; + + const PROJECT = 'test-project'; + const BUILD_TARGET: BuildTarget = { + name: `${PROJECT}:build:production` + }; + + beforeEach(() => { + // Save and clear CI environment variables to ensure consistent test behavior + originalEnv = { ...process.env }; + delete process.env.TRAVIS; + delete process.env.CIRCLECI; + delete process.env.GITHUB_ACTIONS; + + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + capturedPublishOptions = null; + context = createMockContext(); + }); + + afterEach(() => { + // Restore original environment + process.env = originalEnv; + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + }); + + describe('Boolean negation transformation (CRITICAL)', () => { + it('should transform noDotfiles: true to dotfiles: false in complete flow', async () => { + const options: Schema = { + repo: 'https://github.com/test/repo.git', + noDotfiles: true, + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.dotfiles).toBe(false); + // Internal options should NOT be passed to gh-pages + expect(capturedPublishOptions!.noDotfiles).toBeUndefined(); + }); + + it('should transform noNotfound: true to notfound: false in complete flow', async () => { + const options: Schema = { + repo: 'https://github.com/test/repo.git', + noNotfound: true, + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + // notfound is internal to angular-cli-ghpages, NOT passed to gh-pages + // (notfound controls 404.html creation which we do ourselves) + expect(capturedPublishOptions!.notfound).toBeUndefined(); + expect(capturedPublishOptions!.noNotfound).toBeUndefined(); + }); + + it('should transform noNojekyll: true to nojekyll: false in complete flow', async () => { + const options: Schema = { + repo: 'https://github.com/test/repo.git', + noNojekyll: true, + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + // nojekyll IS passed to gh-pages v6+ (delegated to gh-pages) + expect(capturedPublishOptions!.nojekyll).toBe(false); + expect(capturedPublishOptions!.noNojekyll).toBeUndefined(); + }); + + it('should transform all three negation flags together in complete flow', async () => { + const options: Schema = { + repo: 'https://github.com/test/repo.git', + noDotfiles: true, + noNotfound: true, + noNojekyll: true, + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.dotfiles).toBe(false); + // nojekyll IS passed to gh-pages v6+ (delegated) + expect(capturedPublishOptions!.nojekyll).toBe(false); + // notfound is internal (404.html creation by angular-cli-ghpages) + expect(capturedPublishOptions!.notfound).toBeUndefined(); + // negated options should NOT be passed + expect(capturedPublishOptions!.noDotfiles).toBeUndefined(); + expect(capturedPublishOptions!.noNotfound).toBeUndefined(); + expect(capturedPublishOptions!.noNojekyll).toBeUndefined(); + }); + + it('should default all boolean flags to true when not specified', async () => { + const options: Schema = { + repo: 'https://github.com/test/repo.git', + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.dotfiles).toBe(true); + // nojekyll IS passed to gh-pages v6+ (delegated) + expect(capturedPublishOptions!.nojekyll).toBe(true); + // notfound is internal (404.html creation by angular-cli-ghpages) + expect(capturedPublishOptions!.notfound).toBeUndefined(); + }); + }); + + describe('String parameter passthrough (complete flow)', () => { + it('should pass repo through complete flow unchanged', async () => { + const repo = 'https://github.com/test/repo.git'; + const options: Schema = { repo, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.repo).toBe(repo); + }); + + it('should pass branch through complete flow unchanged', async () => { + const repo = 'https://github.com/test/repo.git'; + const branch = 'production'; + const options: Schema = { repo, branch, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.branch).toBe(branch); + }); + + it('should pass message through complete flow unchanged', async () => { + const repo = 'https://github.com/test/repo.git'; + const message = 'Custom deployment message'; + const options: Schema = { repo, message, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.message).toBe(message); + }); + + it('should transform name and email into user object in complete flow', async () => { + const repo = 'https://github.com/test/repo.git'; + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + const expectedUser = { name, email }; + const options: Schema = { repo, name, email, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.user).toEqual(expectedUser); + }); + + it('should pass cname to gh-pages v6+ (delegated to gh-pages)', async () => { + const repo = 'https://github.com/test/repo.git'; + const cname = 'example.com'; + const options: Schema = { repo, cname, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + // cname IS now passed to gh-pages v6+ (delegated file creation) + expect(capturedPublishOptions!.cname).toBe(cname); + }); + }); + + describe('Boolean parameter passthrough (non-negated)', () => { + it('should pass add flag through complete flow unchanged', async () => { + const repo = 'https://github.com/test/repo.git'; + const add = true; + const options: Schema = { repo, add, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + expect(capturedPublishOptions!.add).toBe(add); + }); + + it('should not call gh-pages.publish when dryRun is true', async () => { + const ghPages = require('gh-pages'); + + // Reset mock to clear calls from previous tests + ghPages.publish.mockClear(); + + const repo = 'https://github.com/test/repo.git'; + const options: Schema = { repo, dryRun: true, noBuild: true }; + + await deploy(engine, context, BUILD_TARGET, options); + + // Verify publish was not called in this test + expect(ghPages.publish).not.toHaveBeenCalled(); + }); + }); + + describe('Complete integration with all parameters', () => { + it('should handle all parameters together in complete flow', async () => { + const repo = 'https://github.com/test/repo.git'; + const remote = 'upstream'; + const branch = 'production'; + const message = 'Full integration test'; + const name = 'Integration Bot'; + const email = 'bot@test.com'; + const cname = 'test.com'; + const add = true; + + const options: Schema = { + repo, + remote, + branch, + message, + name, + email, + cname, + add, + noDotfiles: true, + noNotfound: true, + noNojekyll: true, + noBuild: true + }; + + await deploy(engine, context, BUILD_TARGET, options); + + expect(capturedPublishOptions).not.toBeNull(); + + // Verify passthrough parameters + expect(capturedPublishOptions!.repo).toBe(repo); + expect(capturedPublishOptions!.remote).toBe(remote); + expect(capturedPublishOptions!.branch).toBe(branch); + expect(capturedPublishOptions!.message).toBe(message); + expect(capturedPublishOptions!.add).toBe(add); + + // cname and nojekyll ARE passed to gh-pages v6+ (delegated) + expect(capturedPublishOptions!.cname).toBe(cname); + expect(capturedPublishOptions!.nojekyll).toBe(false); + + // notfound is internal (404.html creation by angular-cli-ghpages) + expect(capturedPublishOptions!.notfound).toBeUndefined(); + + // Negated options should NOT be passed + expect(capturedPublishOptions!.noDotfiles).toBeUndefined(); + expect(capturedPublishOptions!.noNotfound).toBeUndefined(); + expect(capturedPublishOptions!.noNojekyll).toBeUndefined(); + expect(capturedPublishOptions!.name).toBeUndefined(); + expect(capturedPublishOptions!.email).toBeUndefined(); + + // Verify user object transformation + expect(capturedPublishOptions!.user).toEqual({ name, email }); + + // Verify boolean negation transformations + expect(capturedPublishOptions!.dotfiles).toBe(false); + + // Verify engine defaults are set + expect(capturedPublishOptions!.git).toBe('git'); + }); + }); + +}); + +function createMockContext(): BuilderContext { + const mockContext: Partial = { + target: { + configuration: 'production', + project: 'test-project', + target: 'deploy' + }, + builder: { + builderName: 'angular-cli-ghpages:deploy', + description: 'Deploy to GitHub Pages', + optionSchema: false + }, + currentDirectory: '/test', + id: 1, + logger: new logging.NullLogger(), + workspaceRoot: '/test', + addTeardown: _ => {}, + validateOptions: (_options: JsonObject, _builderName: string) => Promise.resolve({} as T), + getBuilderNameForTarget: () => Promise.resolve(''), + getTargetOptions: (_: Target) => + Promise.resolve({ + outputPath: 'dist/some-folder' + } as JsonObject), + reportProgress: (_: number, __?: number, ___?: string) => {}, + reportStatus: (_: string) => {}, + reportRunning: () => {}, + scheduleBuilder: (_: string, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({} as BuilderRun), + scheduleTarget: (_: Target, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({ + result: Promise.resolve({ + success: true, + error: '', + info: {}, + target: {} as Target + }) + } as BuilderRun) + }; + + return mockContext as BuilderContext; +} diff --git a/src/parameter-tests/builder-passthrough.spec.ts b/src/parameter-tests/builder-passthrough.spec.ts new file mode 100644 index 0000000..88c12b4 --- /dev/null +++ b/src/parameter-tests/builder-passthrough.spec.ts @@ -0,0 +1,415 @@ +import { BuilderContext, BuilderRun, ScheduleOptions, Target } from '@angular-devkit/architect/src'; +import { JsonObject, logging } from '@angular-devkit/core'; +import deploy from '../deploy/actions'; +import { BuildTarget } from '../interfaces'; +import { Schema } from '../deploy/schema'; + +/** + * ANGULAR BUILDER PARAMETER PASSTHROUGH TESTS + * + * Tests that parameters from angular.json configuration are correctly passed + * through the Angular builder to engine.run(). + * + * Flow: angular.json β†’ deploy/actions.ts β†’ engine.run() + */ + +describe('Angular Builder Parameter Passthrough', () => { + let context: BuilderContext; + let capturedDir: string | null = null; + let capturedOptions: Schema | null = null; + + const PROJECT = 'test-project'; + const BUILD_TARGET: BuildTarget = { + name: `${PROJECT}:build:production` + }; + + const mockEngine = { + run: (dir: string, options: Schema, logger: logging.LoggerApi) => { + capturedDir = dir; + capturedOptions = options; + return Promise.resolve(); + } + }; + + beforeEach(() => { + capturedDir = null; + capturedOptions = null; + context = createMockContext(); + }); + + describe('Basic parameter passing', () => { + it('should pass repo parameter', async () => { + const repo = 'https://github.com/test/repo.git'; + const options: Schema = { repo, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.repo).toBe(repo); + }); + + it('should pass remote parameter', async () => { + const remote = 'upstream'; + const options: Schema = { remote, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.remote).toBe(remote); + }); + + it('should pass message parameter', async () => { + const message = 'Custom deployment message'; + const options: Schema = { message, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.message).toBe(message); + }); + + it('should pass branch parameter', async () => { + const branch = 'production'; + const options: Schema = { branch, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.branch).toBe(branch); + }); + + it('should pass name and email parameters together', async () => { + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + const options: Schema = { name, email, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.name).toBe(name); + expect(capturedOptions!.email).toBe(email); + }); + + it('should pass cname parameter', async () => { + const cname = 'example.com'; + const options: Schema = { cname, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.cname).toBe(cname); + }); + + it('should pass add flag', async () => { + const add = true; + const options: Schema = { add, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.add).toBe(add); + }); + + it('should pass dryRun flag', async () => { + const dryRun = true; + const options: Schema = { dryRun, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.dryRun).toBe(dryRun); + }); + }); + + describe('Boolean flags with no- prefix', () => { + it('should pass noDotfiles flag', async () => { + const noDotfiles = true; + const options: Schema = { noDotfiles, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.noDotfiles).toBe(noDotfiles); + }); + + it('should pass noNotfound flag', async () => { + const noNotfound = true; + const options: Schema = { noNotfound, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.noNotfound).toBe(noNotfound); + }); + + it('should pass noNojekyll flag', async () => { + const noNojekyll = true; + const options: Schema = { noNojekyll, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.noNojekyll).toBe(noNojekyll); + }); + + it('should pass all three no- flags together', async () => { + const noDotfiles = true; + const noNotfound = true; + const noNojekyll = true; + const options: Schema = { noDotfiles, noNotfound, noNojekyll, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.noDotfiles).toBe(noDotfiles); + expect(capturedOptions!.noNotfound).toBe(noNotfound); + expect(capturedOptions!.noNojekyll).toBe(noNojekyll); + }); + }); + + describe('Directory handling', () => { + it('should pass custom dir parameter when provided', async () => { + const dir = 'dist/custom'; + const options: Schema = { dir, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedDir).toBe(dir); + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.dir).toBe(dir); + }); + + it('should use default outputPath/browser when dir not provided', async () => { + const expectedDir = 'dist/some-folder/browser'; + const options: Schema = { noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedDir).toBe(expectedDir); + }); + }); + + describe('All parameters together', () => { + it('should pass all parameters simultaneously', async () => { + const dir = 'dist/custom'; + const repo = 'https://github.com/test/repo.git'; + const remote = 'upstream'; + const branch = 'production'; + const message = 'Full deploy'; + const name = 'Bot'; + const email = 'bot@test.com'; + const cname = 'test.com'; + const add = true; + const dryRun = true; + const noDotfiles = true; + const noNotfound = true; + const noNojekyll = true; + + const options: Schema = { + dir, + repo, + remote, + branch, + message, + name, + email, + cname, + add, + dryRun, + noDotfiles, + noNotfound, + noNojekyll, + noBuild: true + }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.dir).toBe(dir); + expect(capturedOptions!.repo).toBe(repo); + expect(capturedOptions!.remote).toBe(remote); + expect(capturedOptions!.branch).toBe(branch); + expect(capturedOptions!.message).toBe(message); + expect(capturedOptions!.name).toBe(name); + expect(capturedOptions!.email).toBe(email); + expect(capturedOptions!.cname).toBe(cname); + expect(capturedOptions!.add).toBe(add); + expect(capturedOptions!.dryRun).toBe(dryRun); + expect(capturedOptions!.noDotfiles).toBe(noDotfiles); + expect(capturedOptions!.noNotfound).toBe(noNotfound); + expect(capturedOptions!.noNojekyll).toBe(noNojekyll); + }); + }); + + describe('baseHref parameter', () => { + it('should pass baseHref parameter', async () => { + const baseHref = '/my-app/'; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + + it('should handle baseHref with trailing slash', async () => { + const baseHref = '/app/'; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + // Passthrough - same variable proves no transformation + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + + it('should handle baseHref without trailing slash', async () => { + const baseHref = '/app'; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + // Passthrough - same variable proves no transformation + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + + it('should handle empty baseHref', async () => { + const baseHref = ''; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + // Even empty string should pass through + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + + it('should handle absolute URL as baseHref', async () => { + const baseHref = 'https://example.com/app/'; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + // Passthrough - absolute URLs allowed + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + + it('should handle baseHref with special characters', async () => { + const baseHref = '/my-app_v2.0/'; + const options: Schema = { baseHref, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.baseHref).toBe(baseHref); + }); + }); + + describe('Special values', () => { + it('should handle URLs correctly', async () => { + const repo = 'https://github.com/org/repo-name.git'; + const options: Schema = { repo, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.repo).toBe(repo); + }); + + it('should handle branch names with slashes', async () => { + const branch = 'feature/new-feature'; + const options: Schema = { branch, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.branch).toBe(branch); + }); + + it('should handle email with plus addressing', async () => { + const name = 'User'; + const email = 'user+deploy@example.com'; + const options: Schema = { name, email, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.name).toBe(name); + expect(capturedOptions!.email).toBe(email); + }); + + it('should handle subdomain in cname', async () => { + const cname = 'app.subdomain.example.com'; + const options: Schema = { cname, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.cname).toBe(cname); + }); + + it('should handle message with quotes', async () => { + const message = 'Deploy "version 2.0"'; + const options: Schema = { message, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.message).toBe(message); + }); + + it('should handle message with unicode characters', async () => { + const message = 'Deploy πŸš€ with emojis ✨'; + const options: Schema = { message, noBuild: true }; + + await deploy(mockEngine, context, BUILD_TARGET, options); + + expect(capturedOptions).not.toBeNull(); + expect(capturedOptions!.message).toBe(message); + }); + }); +}); + +function createMockContext(): BuilderContext { + const mockContext: Partial = { + target: { + configuration: 'production', + project: 'test-project', + target: 'deploy' + }, + builder: { + builderName: 'angular-cli-ghpages:deploy', + description: 'Deploy to GitHub Pages', + optionSchema: false + }, + currentDirectory: '/test', + id: 1, + logger: new logging.NullLogger(), + workspaceRoot: '/test', + addTeardown: _ => {}, + validateOptions: (_options: JsonObject, _builderName: string) => Promise.resolve({} as T), + getBuilderNameForTarget: () => Promise.resolve(''), + getTargetOptions: (_: Target) => + Promise.resolve({ + outputPath: 'dist/some-folder' + } as JsonObject), + reportProgress: (_: number, __?: number, ___?: string) => {}, + reportStatus: (_: string) => {}, + reportRunning: () => {}, + scheduleBuilder: (_: string, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({} as BuilderRun), + scheduleTarget: (_: Target, __?: JsonObject, ___?: ScheduleOptions) => + Promise.resolve({ + result: Promise.resolve({ + success: true, + error: '', + info: {}, + target: {} as Target + }) + } as BuilderRun) + }; + + return mockContext as BuilderContext; +} diff --git a/src/parameter-tests/cli-e2e.spec.ts b/src/parameter-tests/cli-e2e.spec.ts new file mode 100644 index 0000000..51aa0e4 --- /dev/null +++ b/src/parameter-tests/cli-e2e.spec.ts @@ -0,0 +1,390 @@ +import path from 'path'; +import fs from 'fs'; +import { execSync } from 'child_process'; + +/** + * END-TO-END CLI TESTS + * + * Tests the standalone CLI (angular-cli-ghpages script) to ensure + * commander.js correctly parses all arguments and passes them to the engine. + * + * These tests are CRITICAL for detecting commander.js version regressions. + */ + +interface DryRunOutput { + dir: string; + repo?: string; + remote?: string; + branch?: string; + message?: string; + name?: string; + email?: string; + dotfiles?: string; + notfound?: string; + nojekyll?: string; + cname?: string; + add?: string; +} + +function runCli(args: string): string { + const distFolder = path.resolve(__dirname, '../dist'); + + if (!fs.existsSync(distFolder)) { + throw new Error(`Dist directory ${distFolder} not found. Run npm run build first.`); + } + + const program = path.resolve(__dirname, '../dist/angular-cli-ghpages'); + // Clear CI environment variables to ensure consistent test behavior across environments + const env = { ...process.env }; + delete env.TRAVIS; + delete env.CIRCLECI; + delete env.GITHUB_ACTIONS; + + return execSync(`node ${program} --dry-run ${args}`, { env }).toString(); +} + +function parseJsonFromCliOutput(output: string): DryRunOutput { + // Extract the JSON object from the dry-run output + const jsonMatch = output.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error(`Could not find JSON in CLI output. Output was:\n${output.substring(0, 500)}`); + } + try { + return JSON.parse(jsonMatch[0]) as DryRunOutput; + } catch (parseError) { + const errorMessage = parseError instanceof Error ? parseError.message : String(parseError); + throw new Error(`Failed to parse JSON from CLI output: ${errorMessage}\nJSON string: ${jsonMatch[0].substring(0, 200)}`); + } +} + +describe('CLI End-to-End Tests', () => { + describe('Basic parameter passing', () => { + it('should pass dir parameter', () => { + const dir = 'dist'; + const expectedDir = path.resolve(__dirname, '..', dir); + const output = runCli(`--dir=${dir}`); + const json = parseJsonFromCliOutput(output); + + expect(json.dir).toBe(expectedDir); + }); + + it('should pass repo parameter', () => { + const repo = 'https://github.com/test/repo.git'; + const output = runCli(`--repo=${repo}`); + const json = parseJsonFromCliOutput(output); + + expect(json.repo).toBe(repo); + }); + + it('should pass remote parameter with repo', () => { + const repo = 'https://github.com/test/repo.git'; + const remote = 'upstream'; + const output = runCli(`--repo=${repo} --remote=${remote}`); + const json = parseJsonFromCliOutput(output); + + expect(json.remote).toBe(remote); + }); + + it('should pass message parameter with quotes', () => { + const message = 'Custom deploy message'; + const output = runCli(`--message="${message}"`); + const json = parseJsonFromCliOutput(output); + + expect(json.message).toBe(message); + }); + + it('should pass branch parameter', () => { + const branch = 'production'; + const output = runCli(`--branch=${branch}`); + const json = parseJsonFromCliOutput(output); + + expect(json.branch).toBe(branch); + }); + + // Note: name and email must BOTH be provided together to create user object + it('should pass name and email parameters together', () => { + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + const expectedNameMessage = `the name '${name}' will be used for the commit`; + const expectedEmailMessage = `the email '${email}' will be used for the commit`; + + const output = runCli(`--name="${name}" --email=${email}`); + const json = parseJsonFromCliOutput(output); + + expect(json.name).toBe(expectedNameMessage); + expect(json.email).toBe(expectedEmailMessage); + }); + + it('should pass cname parameter', () => { + const cname = 'example.com'; + const expectedMessage = `a CNAME file with the content '${cname}' will be created`; + const output = runCli(`--cname=${cname}`); + const json = parseJsonFromCliOutput(output); + + expect(json.cname).toBe(expectedMessage); + }); + + it('should pass add flag', () => { + const expectedMessage = 'all files will be added to the branch. Existing files will not be removed'; + const output = runCli('--add'); + const json = parseJsonFromCliOutput(output); + + expect(json.add).toBe(expectedMessage); + }); + }); + + describe('Boolean flags with --no- prefix', () => { + it('should handle --no-dotfiles flag', () => { + const expectedDotfiles = "files starting with dot ('.') will be ignored"; + const output = runCli('--no-dotfiles'); + const json = parseJsonFromCliOutput(output); + + expect(json.dotfiles).toBe(expectedDotfiles); + }); + + it('should handle --no-notfound flag', () => { + const expectedNotfound = 'a 404.html file will NOT be created'; + const output = runCli('--no-notfound'); + const json = parseJsonFromCliOutput(output); + + expect(json.notfound).toBe(expectedNotfound); + }); + + it('should handle --no-nojekyll flag', () => { + const expectedNojekyll = 'a .nojekyll file will NOT be created'; + const output = runCli('--no-nojekyll'); + const json = parseJsonFromCliOutput(output); + + expect(json.nojekyll).toBe(expectedNojekyll); + }); + + it('should handle all three --no- flags together', () => { + const expectedDotfiles = "files starting with dot ('.') will be ignored"; + const expectedNotfound = 'a 404.html file will NOT be created'; + const expectedNojekyll = 'a .nojekyll file will NOT be created'; + + const output = runCli('--no-dotfiles --no-notfound --no-nojekyll'); + const json = parseJsonFromCliOutput(output); + + expect(json.dotfiles).toBe(expectedDotfiles); + expect(json.notfound).toBe(expectedNotfound); + expect(json.nojekyll).toBe(expectedNojekyll); + }); + }); + + describe('Multiple parameters combined', () => { + it('should handle multiple parameters correctly', () => { + const dir = 'dist'; + const repo = 'https://github.com/test/repo.git'; + const branch = 'main'; + const message = 'Deploy to main'; + const expectedDir = path.resolve(__dirname, '..', dir); + + const output = runCli(`--dir=${dir} --repo=${repo} --branch=${branch} --message="${message}"`); + const json = parseJsonFromCliOutput(output); + + expect(json.dir).toBe(expectedDir); + expect(json.repo).toBe(repo); + expect(json.branch).toBe(branch); + expect(json.message).toBe(message); + }); + + it('should handle all parameters at once', () => { + const dir = 'dist'; + const repo = 'https://github.com/test/repo.git'; + const remote = 'upstream'; + const branch = 'production'; + const message = 'Full deploy'; + const name = 'Bot'; + const email = 'bot@test.com'; + const cname = 'test.com'; + const expectedDir = path.resolve(__dirname, '..', dir); + const expectedNameMessage = `the name '${name}' will be used for the commit`; + const expectedEmailMessage = `the email '${email}' will be used for the commit`; + const expectedCnameMessage = `a CNAME file with the content '${cname}' will be created`; + const expectedAddMessage = 'all files will be added to the branch. Existing files will not be removed'; + const expectedDotfiles = "files starting with dot ('.') will be ignored"; + const expectedNotfound = 'a 404.html file will NOT be created'; + const expectedNojekyll = 'a .nojekyll file will NOT be created'; + + const output = runCli( + `--dir=${dir} ` + + `--repo=${repo} ` + + `--remote=${remote} ` + + `--branch=${branch} ` + + `--message="${message}" ` + + `--name="${name}" ` + + `--email=${email} ` + + `--cname=${cname} ` + + `--add ` + + `--no-dotfiles ` + + `--no-notfound ` + + `--no-nojekyll` + ); + const json = parseJsonFromCliOutput(output); + + expect(json.dir).toBe(expectedDir); + expect(json.repo).toBe(repo); + expect(json.remote).toBe(remote); + expect(json.branch).toBe(branch); + expect(json.message).toBe(message); + expect(json.name).toBe(expectedNameMessage); + expect(json.email).toBe(expectedEmailMessage); + expect(json.cname).toBe(expectedCnameMessage); + expect(json.add).toBe(expectedAddMessage); + expect(json.dotfiles).toBe(expectedDotfiles); + expect(json.notfound).toBe(expectedNotfound); + expect(json.nojekyll).toBe(expectedNojekyll); + }); + }); + + describe('Short flags', () => { + it('should accept -d short flag for dir', () => { + const dir = 'dist'; + const expectedDir = path.resolve(__dirname, '..', dir); + const output = runCli(`-d ${dir}`); + const json = parseJsonFromCliOutput(output); + + expect(json.dir).toBe(expectedDir); + }); + + it('should accept -r short flag for repo', () => { + const repo = 'https://github.com/test/short.git'; + const output = runCli(`-r ${repo}`); + const json = parseJsonFromCliOutput(output); + + expect(json.repo).toBe(repo); + }); + + it('should accept -m short flag for message', () => { + const message = 'Short message'; + const output = runCli(`-m "${message}"`); + const json = parseJsonFromCliOutput(output); + + expect(json.message).toBe(message); + }); + + it('should accept -b short flag for branch', () => { + const branch = 'dev'; + const output = runCli(`-b ${branch}`); + const json = parseJsonFromCliOutput(output); + + expect(json.branch).toBe(branch); + }); + + // Note: name and email must BOTH be provided together + it('should accept -n and -e short flags together', () => { + const name = 'Short Name'; + const email = 'short@test.com'; + const expectedNameMessage = `the name '${name}' will be used for the commit`; + const expectedEmailMessage = `the email '${email}' will be used for the commit`; + + const output = runCli(`-n "${name}" -e ${email}`); + const json = parseJsonFromCliOutput(output); + + expect(json.name).toBe(expectedNameMessage); + expect(json.email).toBe(expectedEmailMessage); + }); + + it('should accept -c short flag for cname', () => { + const cname = 'short.com'; + const expectedMessage = `a CNAME file with the content '${cname}' will be created`; + const output = runCli(`-c ${cname}`); + const json = parseJsonFromCliOutput(output); + + expect(json.cname).toBe(expectedMessage); + }); + + it('should accept -a short flag for add', () => { + const expectedMessage = 'all files will be added to the branch. Existing files will not be removed'; + const output = runCli('-a'); + const json = parseJsonFromCliOutput(output); + + expect(json.add).toBe(expectedMessage); + }); + }); + + describe('Parameter formats', () => { + it('should handle --param=value format', () => { + const branch = 'equals-format'; + const output = runCli(`--branch=${branch}`); + const json = parseJsonFromCliOutput(output); + + expect(json.branch).toBe(branch); + }); + + it('should handle --param value format', () => { + const branch = 'space-format'; + const output = runCli(`--branch ${branch}`); + const json = parseJsonFromCliOutput(output); + + expect(json.branch).toBe(branch); + }); + + it('should handle message with spaces in quotes', () => { + const message = 'Message with multiple spaces'; + const output = runCli(`--message="${message}"`); + const json = parseJsonFromCliOutput(output); + + expect(json.message).toBe(message); + }); + + it('should handle message with single quotes', () => { + const message = 'Single quoted message'; + const output = runCli(`--message='${message}'`); + const json = parseJsonFromCliOutput(output); + + expect(json.message).toBe(message); + }); + }); + + describe('Special values', () => { + it('should handle paths with slashes', () => { + const dir = 'deploy'; + const expectedDir = path.resolve(__dirname, '..', dir); + const output = runCli(`--dir=${dir}`); + const json = parseJsonFromCliOutput(output); + + expect(json.dir).toBe(expectedDir); + }); + + it('should handle URLs correctly', () => { + const repo = 'https://github.com/org/repo-name.git'; + const output = runCli(`--repo=${repo}`); + const json = parseJsonFromCliOutput(output); + + expect(json.repo).toBe(repo); + }); + + it('should handle branch names with slashes', () => { + const branch = 'feature/new-feature'; + const output = runCli(`--branch=${branch}`); + const json = parseJsonFromCliOutput(output); + + expect(json.branch).toBe(branch); + }); + + // Note: email must be provided with name + it('should handle email with plus addressing', () => { + const name = 'User'; + const email = 'user+deploy@example.com'; + const expectedNameMessage = `the name '${name}' will be used for the commit`; + const expectedEmailMessage = `the email '${email}' will be used for the commit`; + + const output = runCli(`--name="${name}" --email=${email}`); + const json = parseJsonFromCliOutput(output); + + expect(json.name).toBe(expectedNameMessage); + expect(json.email).toBe(expectedEmailMessage); + }); + + it('should handle subdomain in cname', () => { + const cname = 'app.subdomain.example.com'; + const expectedMessage = `a CNAME file with the content '${cname}' will be created`; + const output = runCli(`--cname=${cname}`); + const json = parseJsonFromCliOutput(output); + + expect(json.cname).toBe(expectedMessage); + }); + }); +}); diff --git a/src/parameter-tests/edge-cases.spec.ts b/src/parameter-tests/edge-cases.spec.ts new file mode 100644 index 0000000..8e09c57 --- /dev/null +++ b/src/parameter-tests/edge-cases.spec.ts @@ -0,0 +1,469 @@ +import { logging } from '@angular-devkit/core'; +import * as engine from '../engine/engine'; +import { cleanupMonkeypatch } from '../engine/engine.prepare-options-helpers'; +import { DeployUser } from '../interfaces'; + +/** + * EDGE CASE TEST SUITE + * + * Tests unusual, boundary, and potentially problematic inputs + * to ensure robustness across all scenarios. + * + * Categories: + * - Empty/null/undefined values + * - Special characters + * - Path edge cases + * - Token injection edge cases + * - Multiple environment variable scenarios + */ + +describe('Edge Case Tests', () => { + let logger: logging.LoggerApi; + const originalEnv = process.env; + + beforeEach(() => { + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + + logger = new logging.NullLogger(); + // Create fresh copy of environment for each test + // This preserves PATH, HOME, etc. needed by git + process.env = { ...originalEnv }; + // Clear only CI-specific vars we're testing + delete process.env.TRAVIS; + delete process.env.TRAVIS_COMMIT_MESSAGE; + delete process.env.TRAVIS_REPO_SLUG; + delete process.env.TRAVIS_COMMIT; + delete process.env.TRAVIS_BUILD_WEB_URL; + delete process.env.CIRCLECI; + delete process.env.CIRCLE_PROJECT_USERNAME; + delete process.env.CIRCLE_PROJECT_REPONAME; + delete process.env.CIRCLE_SHA1; + delete process.env.CIRCLE_BUILD_URL; + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_REPOSITORY; + delete process.env.GITHUB_SHA; + delete process.env.GITHUB_RUN_ID; + delete process.env.GITHUB_SERVER_URL; + delete process.env.GH_TOKEN; + delete process.env.PERSONAL_TOKEN; + delete process.env.GITHUB_TOKEN; + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + // Restore original environment for other test files + process.env = originalEnv; + }); + + describe('Empty/null/undefined handling', () => { + it('should handle empty string for repo', async () => { + const options = { repo: '' }; + + // Empty repo should likely discover from git + await expect(engine.prepareOptions(options, logger)).resolves.toBeDefined(); + }); + + it('should handle empty string for message', async () => { + const options = { message: '' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe(''); + }); + + it('should handle empty string for branch', async () => { + const options = { branch: '' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.branch).toBe(''); + }); + + it('should handle empty string for cname', async () => { + const options = { cname: '' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.cname).toBe(''); + }); + + it('should handle undefined values for optional parameters', async () => { + const options = { + repo: undefined, + message: undefined, + branch: undefined, + cname: undefined + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Should apply defaults where appropriate + expect(finalOptions).toBeDefined(); + }); + }); + + describe('Special characters in parameters', () => { + it('should handle commit message with quotes', async () => { + const options = { message: 'Deploy "version 2.0"' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe('Deploy "version 2.0"'); + }); + + it('should handle commit message with single quotes', async () => { + const options = { message: "Deploy 'version 2.0'" }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe("Deploy 'version 2.0'"); + }); + + it('should handle commit message with newlines', async () => { + const options = { message: 'Line 1\nLine 2\nLine 3' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe('Line 1\nLine 2\nLine 3'); + }); + + it('should handle commit message with unicode characters', async () => { + const options = { message: 'Deploy πŸš€ with emojis ✨' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe('Deploy πŸš€ with emojis ✨'); + }); + + it('should handle name with unicode', async () => { + const options = { + name: 'ε±±η”°ε€ͺιƒŽ', + email: 'yamada@example.jp' + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + const user = finalOptions.user as DeployUser | undefined; + expect(user).toBeDefined(); + expect(user?.name).toBe('ε±±η”°ε€ͺιƒŽ'); + }); + + it('should handle email with plus addressing', async () => { + const options = { + name: 'Test User', + email: 'user+deploy@example.com' + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + const user = finalOptions.user as DeployUser | undefined; + expect(user).toBeDefined(); + expect(user?.email).toBe('user+deploy@example.com'); + }); + + it('should handle branch name with slashes', async () => { + const options = { branch: 'feature/new-feature' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.branch).toBe('feature/new-feature'); + }); + + it('should handle cname with international domain', async () => { + const options = { cname: 'mΓΌnchen.de' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.cname).toBe('mΓΌnchen.de'); + }); + }); + + describe('Path edge cases', () => { + it('should handle paths with spaces', async () => { + const options = { dir: 'dist/my app/folder' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dir).toBe('dist/my app/folder'); + }); + + it('should handle absolute paths', async () => { + const options = { dir: '/absolute/path/to/dist' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dir).toBe('/absolute/path/to/dist'); + }); + + it('should handle paths with dots', async () => { + const options = { dir: '../dist' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dir).toBe('../dist'); + }); + + it('should handle Windows-style paths', async () => { + const options = { dir: 'C:\\Users\\test\\dist' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dir).toBe('C:\\Users\\test\\dist'); + }); + + it('should handle git executable path with spaces', async () => { + const options = { git: '/Program Files/Git/bin/git', repo: 'https://github.com/test/repo.git' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.git).toBe('/Program Files/Git/bin/git'); + }, 10000); + }); + + describe('Token injection edge cases', () => { + it('should handle token with special characters', async () => { + const options = { repo: 'https://github.com/test/repo.git' }; + process.env.GH_TOKEN = 'token_with_$pecial_ch@rs!'; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe('https://x-access-token:token_with_$pecial_ch@rs!@github.com/test/repo.git'); + }); + + it('should handle multiple tokens set - GH_TOKEN takes precedence', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'gh_token'; + const expectedUrl = 'https://x-access-token:gh_token@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.GH_TOKEN = token; + process.env.PERSONAL_TOKEN = 'personal_token'; + process.env.GITHUB_TOKEN = 'github_token'; + + const finalOptions = await engine.prepareOptions(options, logger); + + // GH_TOKEN should be used first (based on engine.ts logic) + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should use PERSONAL_TOKEN when GH_TOKEN not set', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'personal_token'; + const expectedUrl = 'https://x-access-token:personal_token@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.PERSONAL_TOKEN = token; + process.env.GITHUB_TOKEN = 'github_token'; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should use GITHUB_TOKEN when others not set', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'github_token'; + const expectedUrl = 'https://x-access-token:github_token@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.GITHUB_TOKEN = token; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should not inject token if repo already has x-access-token', async () => { + const options = { repo: 'https://x-access-token:existing_token@github.com/test/repo.git' }; + process.env.GH_TOKEN = 'new_token'; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Should not inject new token if x-access-token already present + expect(finalOptions.repo).toBe('https://x-access-token:existing_token@github.com/test/repo.git'); + }); + + it('should not inject token into non-GitHub URLs', async () => { + const options = { repo: 'https://gitlab.com/test/repo.git' }; + process.env.GH_TOKEN = 'token'; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Token injection only works for github.com + expect(finalOptions.repo).toBe('https://gitlab.com/test/repo.git'); + }); + + it('should handle repo URL without .git suffix', async () => { + const options = { repo: 'https://github.com/test/repo' }; + process.env.GH_TOKEN = 'token'; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe('https://x-access-token:token@github.com/test/repo'); + }); + + it('should handle empty token environment variable', async () => { + const options = { repo: 'https://github.com/test/repo.git' }; + process.env.GH_TOKEN = ''; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Empty token should NOT be injected (actual behavior - empty string is falsy) + expect(finalOptions.repo).toBe('https://github.com/test/repo.git'); + }); + }); + + describe('CI environment combinations', () => { + it('should handle Travis CI with missing environment variables', async () => { + const message = 'Deploy'; + const expectedMessage = + 'Deploy -- \n\n' + + 'Triggered by commit: https://github.com//commit/\n' + + 'Travis CI build: '; + + const options = { message }; + process.env.TRAVIS = 'true'; + // Missing other Travis env vars (including TRAVIS_BUILD_WEB_URL) + + const finalOptions = await engine.prepareOptions(options, logger); + + // When env vars are undefined, they are replaced with empty strings + expect(finalOptions.message).toBe(expectedMessage); + }); + + it('should handle CircleCI with missing environment variables', async () => { + const message = 'Deploy'; + const expectedMessage = + 'Deploy\n\n' + + 'Triggered by commit: https://github.com///commit/\n' + + 'CircleCI build: '; + + const options = { message }; + process.env.CIRCLECI = 'true'; + // Missing other CircleCI env vars + + const finalOptions = await engine.prepareOptions(options, logger); + + // When env vars are undefined, they are replaced with empty strings + expect(finalOptions.message).toBe(expectedMessage); + }); + + it('should handle GitHub Actions with missing environment variables', async () => { + const message = 'Deploy'; + const expectedMessage = + 'Deploy\n\n' + + 'Triggered by commit: https://github.com//commit/\n' + + 'GitHub Actions build: https://github.com//actions/runs/'; + + const options = { message }; + process.env.GITHUB_ACTIONS = 'true'; + // Missing other GitHub Actions env vars + + const finalOptions = await engine.prepareOptions(options, logger); + + // When env vars are undefined, they are replaced with empty strings + expect(finalOptions.message).toBe(expectedMessage); + }); + + it('should handle multiple CI environments set simultaneously', async () => { + const message = 'Deploy'; + const options = { message }; + process.env.TRAVIS = 'true'; + process.env.CIRCLECI = 'true'; + process.env.GITHUB_ACTIONS = 'true'; + process.env.TRAVIS_COMMIT_MESSAGE = 'Travis commit'; + process.env.CIRCLE_SHA1 = 'circle123'; + process.env.GITHUB_SHA = 'github456'; + + const finalOptions = await engine.prepareOptions(options, logger); + + // With multiple CIs, message gets appended multiple times + // Just verify it doesn't crash and message is modified + expect(finalOptions.message).toBeDefined(); + expect(typeof finalOptions.message).toBe('string'); + if (finalOptions.message) { + expect(finalOptions.message.length).toBeGreaterThan(message.length); + } + }); + }); + + describe('Boolean flag inversions', () => { + it('should handle conflicting dotfiles values (no- flag dominates)', async () => { + const options = { + dotfiles: true, // This shouldn't exist in real usage + noDotfiles: true + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // noDotfiles should take precedence and set dotfiles to false + expect(finalOptions.dotfiles).toBe(false); + }); + + it('should handle all three boolean flags set to no-', async () => { + const options = { + noDotfiles: true, + noNotfound: true, + noNojekyll: true + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dotfiles).toBe(false); + expect(finalOptions.notfound).toBe(false); + expect(finalOptions.nojekyll).toBe(false); + }); + }); + + describe('Extreme values', () => { + it('should handle very long commit message', async () => { + const longMessage = 'A'.repeat(10000); + const options = { message: longMessage }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe(longMessage); + }); + + it('should handle very long repository URL', async () => { + const longUrl = 'https://github.com/' + 'a'.repeat(1000) + '/repo.git'; + const options = { repo: longUrl }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(longUrl); + }); + + it('should handle very long name', async () => { + const longName = 'Name '.repeat(100); + const options = { + name: longName, + email: 'test@example.com' + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + const user = finalOptions.user as DeployUser | undefined; + expect(user).toBeDefined(); + expect(user?.name).toBe(longName); + }); + }); + + describe('Deprecated parameters', () => { + it('should handle deprecated noSilent parameter gracefully and log warning', async () => { + const testLogger = new logging.Logger('test'); + const warnSpy = jest.spyOn(testLogger, 'warn'); + + const options = { noSilent: true }; + const expectedWarning = 'The --no-silent parameter is deprecated and no longer needed. Verbose logging is now always enabled. This parameter will be ignored.'; + + const finalOptions = await engine.prepareOptions(options, testLogger); + + // Verify it doesn't crash and warning is logged + expect(finalOptions).toBeDefined(); + expect(warnSpy).toHaveBeenCalledWith(expectedWarning); + }); + }); +}); diff --git a/src/parameter-tests/parameter-passthrough.spec.ts b/src/parameter-tests/parameter-passthrough.spec.ts new file mode 100644 index 0000000..c3c554d --- /dev/null +++ b/src/parameter-tests/parameter-passthrough.spec.ts @@ -0,0 +1,557 @@ +import { logging } from '@angular-devkit/core'; +import * as engine from '../engine/engine'; +import { cleanupMonkeypatch } from '../engine/engine.prepare-options-helpers'; + +/** + * CRITICAL TEST SUITE: Parameter Passthrough Validation + * + * This suite ensures that ALL parameters are correctly passed from our API + * to gh-pages.publish(). This is essential for upgrading gh-pages and commander + * without introducing regressions. + * + * Testing Philosophy: + * - Use EXPLICIT expected values, never .toContain() for value testing + * - Reuse variables when input === output (passthrough) + * - Use separate input/expected variables when transformation occurs + * - Every assertion documents exact expected behavior + */ + +describe('Parameter Passthrough Tests', () => { + let logger: logging.LoggerApi; + const originalEnv = process.env; + + beforeEach(() => { + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + + logger = new logging.NullLogger(); + // Create fresh copy of environment for each test + // This preserves PATH, HOME, etc. needed by git + process.env = { ...originalEnv }; + // Clear only CI-specific vars we're testing + delete process.env.TRAVIS; + delete process.env.TRAVIS_COMMIT_MESSAGE; + delete process.env.TRAVIS_REPO_SLUG; + delete process.env.TRAVIS_COMMIT; + delete process.env.TRAVIS_BUILD_WEB_URL; + delete process.env.CIRCLECI; + delete process.env.CIRCLE_PROJECT_USERNAME; + delete process.env.CIRCLE_PROJECT_REPONAME; + delete process.env.CIRCLE_SHA1; + delete process.env.CIRCLE_BUILD_URL; + delete process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_REPOSITORY; + delete process.env.GITHUB_SHA; + delete process.env.GITHUB_RUN_ID; + delete process.env.GITHUB_SERVER_URL; + delete process.env.GH_TOKEN; + delete process.env.PERSONAL_TOKEN; + delete process.env.GITHUB_TOKEN; + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + // Restore original environment for other test files + process.env = originalEnv; + }); + + describe('Parameter: repo', () => { + it('should pass repo URL unchanged when no token in environment', async () => { + const repo = 'https://github.com/test/repo.git'; + const options = { repo }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough - same variable proves no transformation + expect(finalOptions.repo).toBe(repo); + }); + + it('should inject GH_TOKEN into HTTPS repo URL', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'secret_token_123'; + const expectedUrl = 'https://x-access-token:secret_token_123@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.GH_TOKEN = token; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should inject PERSONAL_TOKEN into HTTPS repo URL', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'personal_token_456'; + const expectedUrl = 'https://x-access-token:personal_token_456@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.PERSONAL_TOKEN = token; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should inject GITHUB_TOKEN into HTTPS repo URL', async () => { + const inputUrl = 'https://github.com/test/repo.git'; + const token = 'github_token_789'; + const expectedUrl = 'https://x-access-token:github_token_789@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.GITHUB_TOKEN = token; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should NOT inject token into SSH repo URL', async () => { + const repo = 'git@github.com:test/repo.git'; + const options = { repo }; + process.env.GH_TOKEN = 'secret_token_123'; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough - tokens don't work with SSH + expect(finalOptions.repo).toBe(repo); + }); + + it('should handle backwards compatible GH_TOKEN placeholder replacement', async () => { + const inputUrl = 'https://GH_TOKEN@github.com/test/repo.git'; + const token = 'actual_token'; + const expectedUrl = 'https://actual_token@github.com/test/repo.git'; + + const options = { repo: inputUrl }; + process.env.GH_TOKEN = token; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.repo).toBe(expectedUrl); + }); + + it('should discover remote URL when repo not specified', async () => { + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Should discover repo from current git repository + expect(finalOptions.repo).toBeDefined(); + expect(typeof finalOptions.repo).toBe('string'); + if (finalOptions.repo) { + expect(finalOptions.repo.length).toBeGreaterThan(0); + } + }); + }); + + describe('Parameter: remote', () => { + it('should default to origin when not specified', async () => { + const expectedRemote = 'origin'; + const options = { repo: 'https://github.com/test/repo.git' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.remote).toBe(expectedRemote); + }); + + it('should pass custom remote value unchanged', async () => { + const remote = 'upstream'; + const options = { + repo: 'https://github.com/test/repo.git', + remote + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.remote).toBe(remote); + }); + }); + + describe('Parameter: message', () => { + it('should pass custom commit message unchanged', async () => { + const message = 'Custom deployment message'; + const options = { message }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.message).toBe(message); + }); + + it('should append Travis CI metadata to message', async () => { + const baseMessage = 'Deploy'; + const commitMsg = 'Test commit'; + const repoSlug = 'test/repo'; + const commitSha = 'abc123'; + const buildWebUrl = 'https://app.travis-ci.com/test/repo/builds/456'; + const expectedMessage = + 'Deploy -- Test commit \n\n' + + 'Triggered by commit: https://github.com/test/repo/commit/abc123\n' + + 'Travis CI build: https://app.travis-ci.com/test/repo/builds/456'; + + const options = { message: baseMessage }; + process.env.TRAVIS = 'true'; + process.env.TRAVIS_COMMIT_MESSAGE = commitMsg; + process.env.TRAVIS_REPO_SLUG = repoSlug; + process.env.TRAVIS_COMMIT = commitSha; + process.env.TRAVIS_BUILD_WEB_URL = buildWebUrl; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe(expectedMessage); + }); + + it('should append CircleCI metadata to message', async () => { + const baseMessage = 'Deploy'; + const username = 'testuser'; + const reponame = 'testrepo'; + const sha = 'def456'; + const buildUrl = 'https://circleci.com/build/123'; + const expectedMessage = + 'Deploy\n\n' + + 'Triggered by commit: https://github.com/testuser/testrepo/commit/def456\n' + + 'CircleCI build: https://circleci.com/build/123'; + + const options = { message: baseMessage }; + process.env.CIRCLECI = 'true'; + process.env.CIRCLE_PROJECT_USERNAME = username; + process.env.CIRCLE_PROJECT_REPONAME = reponame; + process.env.CIRCLE_SHA1 = sha; + process.env.CIRCLE_BUILD_URL = buildUrl; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe(expectedMessage); + }); + + it('should append GitHub Actions metadata to message', async () => { + const baseMessage = 'Deploy'; + const repository = 'owner/repo'; + const sha = 'ghi789'; + const runId = '12345'; + const expectedMessage = + 'Deploy\n\n' + + 'Triggered by commit: https://github.com/owner/repo/commit/ghi789\n' + + 'GitHub Actions build: https://github.com/owner/repo/actions/runs/12345'; + + const options = { message: baseMessage }; + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_REPOSITORY = repository; + process.env.GITHUB_SHA = sha; + process.env.GITHUB_RUN_ID = runId; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.message).toBe(expectedMessage); + }); + }); + + describe('Parameter: branch', () => { + it('should default to gh-pages', async () => { + const expectedBranch = 'gh-pages'; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.branch).toBe(expectedBranch); + }); + + it('should pass custom branch name unchanged', async () => { + const branch = 'main'; + const options = { branch }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.branch).toBe(branch); + }); + + it('should pass branch name docs unchanged', async () => { + const branch = 'docs'; + const options = { branch }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.branch).toBe(branch); + }); + }); + + describe('Parameter: name and email (user object)', () => { + it('should create user object when both name and email provided', async () => { + const name = 'John Doe'; + const email = 'john@example.com'; + const expectedUser = { name, email }; + + const options = { name, email }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.user).toEqual(expectedUser); + }); + + it('should NOT create user object when only name provided', async () => { + const name = 'John Doe'; + const options = { name }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.user).toBeUndefined(); + expect(finalOptions.name).toBe(name); + }); + + it('should NOT create user object when only email provided', async () => { + const email = 'john@example.com'; + const options = { email }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.user).toBeUndefined(); + expect(finalOptions.email).toBe(email); + }); + + it('should handle name and email with special characters', async () => { + const name = 'JosΓ© GarcΓ­a-MΓΌller'; + const email = 'josΓ©+test@example.com'; + const expectedUser = { name, email }; + + const options = { name, email }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.user).toEqual(expectedUser); + }); + }); + + describe('Parameter: dotfiles (boolean with negation)', () => { + it('should default dotfiles to true', async () => { + const expectedDotfiles = true; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dotfiles).toBe(expectedDotfiles); + }); + + it('should set dotfiles to false when noDotfiles is true', async () => { + const expectedDotfiles = false; + const options = { noDotfiles: true }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dotfiles).toBe(expectedDotfiles); + }); + + it('should keep dotfiles true when noDotfiles is false', async () => { + const expectedDotfiles = true; + const options = { noDotfiles: false }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dotfiles).toBe(expectedDotfiles); + }); + }); + + describe('Parameter: notfound (boolean with negation)', () => { + it('should default notfound to true', async () => { + const expectedNotfound = true; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.notfound).toBe(expectedNotfound); + }); + + it('should set notfound to false when noNotfound is true', async () => { + const expectedNotfound = false; + const options = { noNotfound: true }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.notfound).toBe(expectedNotfound); + }); + + it('should keep notfound true when noNotfound is false', async () => { + const expectedNotfound = true; + const options = { noNotfound: false }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.notfound).toBe(expectedNotfound); + }); + }); + + describe('Parameter: nojekyll (boolean with negation)', () => { + it('should default nojekyll to true', async () => { + const expectedNojekyll = true; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.nojekyll).toBe(expectedNojekyll); + }); + + it('should set nojekyll to false when noNojekyll is true', async () => { + const expectedNojekyll = false; + const options = { noNojekyll: true }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.nojekyll).toBe(expectedNojekyll); + }); + + it('should keep nojekyll true when noNojekyll is false', async () => { + const expectedNojekyll = true; + const options = { noNojekyll: false }; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.nojekyll).toBe(expectedNojekyll); + }); + }); + + describe('Parameter: cname', () => { + it('should default cname to undefined', async () => { + const expectedCname = undefined; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.cname).toBe(expectedCname); + }); + + it('should pass custom cname domain unchanged', async () => { + const cname = 'example.com'; + const options = { cname }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.cname).toBe(cname); + }); + + it('should pass subdomain cname unchanged', async () => { + const cname = 'app.example.com'; + const options = { cname }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.cname).toBe(cname); + }); + }); + + describe('Parameter: add', () => { + it('should default add to false', async () => { + const expectedAdd = false; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.add).toBe(expectedAdd); + }); + + it('should pass add as true when specified', async () => { + const add = true; + const options = { add }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.add).toBe(add); + }); + }); + + describe('Parameter: dryRun', () => { + it('should default dryRun to false', async () => { + const expectedDryRun = false; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.dryRun).toBe(expectedDryRun); + }); + + it('should pass dryRun as true when specified', async () => { + const dryRun = true; + const options = { dryRun }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.dryRun).toBe(dryRun); + }); + }); + + describe('Parameter: git', () => { + it('should default git to git string', async () => { + const expectedGit = 'git'; + const options = {}; + + const finalOptions = await engine.prepareOptions(options, logger); + + expect(finalOptions.git).toBe(expectedGit); + }); + + it('should pass custom git executable path unchanged', async () => { + const git = '/usr/local/bin/git'; + const options = { git, repo: 'https://github.com/test/repo.git' }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Passthrough + expect(finalOptions.git).toBe(git); + }, 10000); + }); + + describe('All parameters together', () => { + it('should handle all parameters specified simultaneously', async () => { + const dir = 'dist/app'; + const repo = 'https://github.com/test/repo.git'; + const remote = 'upstream'; + const message = 'Deploy all the things'; + const branch = 'production'; + const name = 'Deploy Bot'; + const email = 'bot@example.com'; + const cname = 'www.example.com'; + const add = true; + const dryRun = true; + const git = '/custom/git'; + + const options = { + dir, + repo, + remote, + message, + branch, + name, + email, + noDotfiles: true, + noNotfound: true, + noNojekyll: true, + cname, + add, + dryRun, + git + }; + + const finalOptions = await engine.prepareOptions(options, logger); + + // Verify all passthroughs use same variables + expect(finalOptions.dir).toBe(dir); + expect(finalOptions.repo).toBe(repo); + expect(finalOptions.remote).toBe(remote); + expect(finalOptions.message).toBe(message); + expect(finalOptions.branch).toBe(branch); + expect(finalOptions.user).toEqual({ name, email }); + expect(finalOptions.dotfiles).toBe(false); + expect(finalOptions.notfound).toBe(false); + expect(finalOptions.nojekyll).toBe(false); + expect(finalOptions.cname).toBe(cname); + expect(finalOptions.add).toBe(add); + expect(finalOptions.dryRun).toBe(dryRun); + expect(finalOptions.git).toBe(git); + }); + }); +}); diff --git a/src/parameter-tests/pr-186-commander-defaults.spec.ts b/src/parameter-tests/pr-186-commander-defaults.spec.ts new file mode 100644 index 0000000..3b488d9 --- /dev/null +++ b/src/parameter-tests/pr-186-commander-defaults.spec.ts @@ -0,0 +1,193 @@ +/** + * PR #186 COMPATIBILITY TESTS: Commander Boolean Defaults + * + * Context from PR #186 analysis: + * Commander v9+ changed how boolean --no- options handle default values, which would break our CLI. + * Decision: We forked Commander v3 to avoid this breaking change. We will NEVER upgrade to v9+. + * + * Commander v3 behavior (what we use): + * - --no-dotfiles automatically sets dotfiles: false + * - Default is implicitly true (no explicit default needed) + * - This is the behavior we rely on and have locked in via our fork + * + * Our Code (src/angular-cli-ghpages lines 56-67): + * .option('-T, --no-dotfiles', 'Includes dotfiles by default...') + * .option('--no-notfound', 'By default a 404.html file is created...') + * .option('--no-nojekyll', 'By default a .nojekyll file is created...') + * + * Our Defaults (src/engine/defaults.ts): + * dotfiles: true, // Default should be true + * notfound: true, // Default should be true + * nojekyll: true, // Default should be true + * + * What these tests verify: + * - Commander v3 (our fork) + defaults.ts work together correctly + * - When no CLI option is passed, defaults.ts values are used + * - When --no-dotfiles is passed, dotfiles becomes false + * - When --no-notfound is passed, notfound becomes false + * - When --no-nojekyll is passed, nojekyll becomes false + * + * These tests document current behavior and serve as regression tests. + */ + +import { logging } from '@angular-devkit/core'; + +import * as engine from '../engine/engine'; +import { cleanupMonkeypatch } from '../engine/engine.prepare-options-helpers'; +import { defaults } from '../engine/defaults'; + +describe('PR #186 - Commander Boolean Defaults Compatibility', () => { + let logger: logging.LoggerApi; + + beforeEach(() => { + // Clean up any previous monkeypatch so each test starts fresh + cleanupMonkeypatch(); + logger = new logging.NullLogger(); + }); + + afterAll(() => { + // Clean up monkeypatch after all tests + cleanupMonkeypatch(); + }); + + describe('defaults.ts values', () => { + it('should have dotfiles: true as default', () => { + expect(defaults.dotfiles).toBe(true); + }); + + it('should have notfound: true as default', () => { + expect(defaults.notfound).toBe(true); + }); + + it('should have nojekyll: true as default', () => { + expect(defaults.nojekyll).toBe(true); + }); + }); + + describe('prepareOptions - default value application', () => { + it('should apply all defaults when no CLI options provided', async () => { + const userOptions = {}; // User passed no options (typical: ng deploy) + + const result = await engine.prepareOptions(userOptions, logger); + + // CRITICAL: Must be true (from defaults.ts), not undefined! + // If undefined: Files won't be created, breaking deployments: + // - no .nojekyll β†’ Jekyll processes files β†’ breaks Angular app + // - no 404.html β†’ broken SPA routing on GitHub Pages + // - no dotfiles β†’ missing .htaccess, etc. + expect(result.dotfiles).toBe(true); + expect(result.notfound).toBe(true); + expect(result.nojekyll).toBe(true); + }); + + it('should respect noDotfiles: true (negation) to set dotfiles: false', async () => { + // User passed --no-dotfiles flag + const userOptions = { + noDotfiles: true // This means: disable dotfiles + }; + + const result = await engine.prepareOptions(userOptions, logger); + + // CRITICAL: Must be false (negated) + expect(result.dotfiles).toBe(false); + // Others remain at defaults + expect(result.notfound).toBe(true); + expect(result.nojekyll).toBe(true); + }); + + it('should respect noNotfound: true (negation) to set notfound: false', async () => { + // User passed --no-notfound flag (common for Cloudflare Pages) + const userOptions = { + noNotfound: true // This means: disable 404.html creation + }; + + const result = await engine.prepareOptions(userOptions, logger); + + expect(result.notfound).toBe(false); // Negated + expect(result.dotfiles).toBe(true); // Default + expect(result.nojekyll).toBe(true); // Default + }); + + it('should respect noNojekyll: true (negation) to set nojekyll: false', async () => { + // User passed --no-nojekyll flag (rare, but possible) + const userOptions = { + noNojekyll: true // This means: disable .nojekyll creation + }; + + const result = await engine.prepareOptions(userOptions, logger); + + expect(result.nojekyll).toBe(false); // Negated + expect(result.dotfiles).toBe(true); // Default + expect(result.notfound).toBe(true); // Default + }); + + it('should handle two negations simultaneously', async () => { + // User passed --no-dotfiles --no-notfound + const userOptions = { + noDotfiles: true, + noNotfound: true + }; + + const result = await engine.prepareOptions(userOptions, logger); + + expect(result.dotfiles).toBe(false); // Negated + expect(result.notfound).toBe(false); // Negated + expect(result.nojekyll).toBe(true); // Default + }); + + it('should handle all three negations simultaneously', async () => { + // User passed --no-dotfiles --no-notfound --no-nojekyll + const userOptions = { + noDotfiles: true, + noNotfound: true, + noNojekyll: true + }; + + const result = await engine.prepareOptions(userOptions, logger); + + // All negated + expect(result.dotfiles).toBe(false); + expect(result.notfound).toBe(false); + expect(result.nojekyll).toBe(false); + }); + }); + + /** + * Regression tests: Ensure existing deployments continue to work + * + * Most users rely on default behavior (creating .nojekyll and 404.html). + * These tests document critical default behavior that must never break: + * - no .nojekyll = Jekyll processing breaks Angular app + * - no 404.html = broken SPA routing on GitHub Pages + */ + describe('Regression prevention', () => { + it('should maintain default behavior for typical GitHub Pages deployment', async () => { + // Typical user: ng deploy (no options) + const userOptions = {}; + + const result = await engine.prepareOptions(userOptions, logger); + + // CRITICAL: These MUST be true for typical GitHub Pages deployment + // Prevents Jekyll processing that breaks Angular apps with _underscored files + expect(result.nojekyll).toBe(true); + // Enables SPA routing by copying index.html to 404.html + expect(result.notfound).toBe(true); + // Includes dotfiles like .htaccess if present + expect(result.dotfiles).toBe(true); + }); + + it('should maintain default behavior for Cloudflare Pages deployment', async () => { + // Cloudflare Pages user: ng deploy --no-notfound + // (Cloudflare handles 404 routing differently, doesn't need 404.html) + const userOptions = { + noNotfound: true + }; + + const result = await engine.prepareOptions(userOptions, logger); + + expect(result.notfound).toBe(false); // User's explicit choice + expect(result.nojekyll).toBe(true); // Still needed + expect(result.dotfiles).toBe(true); // Still needed + }); + }); +}); diff --git a/src/public-api.spec.ts b/src/public-api.spec.ts new file mode 100644 index 0000000..f291bbe --- /dev/null +++ b/src/public-api.spec.ts @@ -0,0 +1,144 @@ +/** + * Tests for public API exports + * + * Verifies that all public types and functions are correctly exported + * and accessible to consumers of angular-cli-ghpages. + */ + +import * as publicApi from './public_api'; + +describe('Public API', () => { + describe('Schema and Options Types', () => { + it('should allow creating DeployUser objects with correct type', () => { + // DeployUser is a type, we verify it compiles and works correctly + const user: publicApi.DeployUser = { + name: 'Test User', + email: 'test@example.com' + }; + expect(user.name).toBe('Test User'); + expect(user.email).toBe('test@example.com'); + }); + + it('should allow creating AngularOutputPath with string variant', () => { + const outputPath: publicApi.AngularOutputPath = '/dist/my-app'; + expect(typeof outputPath).toBe('string'); + expect(outputPath).toBe('/dist/my-app'); + }); + + it('should allow creating AngularOutputPath with object variant', () => { + const outputPath: publicApi.AngularOutputPath = { + base: '/dist', + browser: 'my-app' + }; + expect(outputPath.base).toBe('/dist'); + expect(outputPath.browser).toBe('my-app'); + }); + + it('should allow creating AngularOutputPathObject', () => { + const outputPath: publicApi.AngularOutputPathObject = { + base: '/dist', + browser: 'browser' + }; + expect(outputPath.base).toBe('/dist'); + expect(outputPath.browser).toBe('browser'); + }); + + it('should export isOutputPathObject type guard function', () => { + expect(typeof publicApi.isOutputPathObject).toBe('function'); + + const stringPath = '/dist/app'; + const objectPath = { base: '/dist', browser: 'app' }; + + expect(publicApi.isOutputPathObject(stringPath)).toBe(false); + expect(publicApi.isOutputPathObject(objectPath)).toBe(true); + }); + }); + + describe('Configuration', () => { + it('should export defaults object', () => { + expect(publicApi.defaults).toBeDefined(); + expect(typeof publicApi.defaults).toBe('object'); + }); + + it('should have expected default values', () => { + expect(publicApi.defaults.dir).toBe('dist'); + expect(publicApi.defaults.branch).toBe('gh-pages'); + expect(publicApi.defaults.remote).toBe('origin'); + expect(publicApi.defaults.dotfiles).toBe(true); + expect(publicApi.defaults.notfound).toBe(true); + expect(publicApi.defaults.nojekyll).toBe(true); + }); + }); + + describe('Core Deployment Functions', () => { + it('should export deployToGHPages function', () => { + expect(typeof publicApi.deployToGHPages).toBe('function'); + }); + + it('should export angularDeploy function', () => { + expect(typeof publicApi.angularDeploy).toBe('function'); + }); + }); + + describe('Advanced Option Processing Functions', () => { + it('should export setupMonkeypatch function', () => { + expect(typeof publicApi.setupMonkeypatch).toBe('function'); + }); + + it('should export mapNegatedBooleans function', () => { + expect(typeof publicApi.mapNegatedBooleans).toBe('function'); + }); + + it('should export handleUserCredentials function', () => { + expect(typeof publicApi.handleUserCredentials).toBe('function'); + }); + + it('should export warnDeprecatedParameters function', () => { + expect(typeof publicApi.warnDeprecatedParameters).toBe('function'); + }); + + it('should export appendCIMetadata function', () => { + expect(typeof publicApi.appendCIMetadata).toBe('function'); + }); + + it('should export injectTokenIntoRepoUrl function', () => { + expect(typeof publicApi.injectTokenIntoRepoUrl).toBe('function'); + }); + + it('should export prepareOptions function', () => { + expect(typeof publicApi.prepareOptions).toBe('function'); + }); + }); + + describe('Example Usage', () => { + it('should allow users to create custom options with defaults', () => { + const customOptions = { + ...publicApi.defaults, + dir: 'custom/dist', + message: 'Custom deployment message', + cname: 'example.com' + }; + + expect(customOptions.dir).toBe('custom/dist'); + expect(customOptions.branch).toBe('gh-pages'); + expect(customOptions.message).toBe('Custom deployment message'); + expect(customOptions.cname).toBe('example.com'); + }); + + it('should allow type-safe user credentials', () => { + const user: publicApi.DeployUser = { + name: 'CI Bot', + email: 'ci@example.com' + }; + + const options = { + ...publicApi.defaults, + name: user.name, + email: user.email + }; + + expect(options.name).toBe('CI Bot'); + expect(options.email).toBe('ci@example.com'); + }); + }); +}); diff --git a/src/public_api.ts b/src/public_api.ts index ad655b2..5bdec14 100644 --- a/src/public_api.ts +++ b/src/public_api.ts @@ -1,3 +1,41 @@ +/** + * Public API for angular-cli-ghpages + * + * This module exports public types and functions that users can use + * to extend or customize angular-cli-ghpages functionality. + */ + +// Angular CLI integration export * from './ng-add'; -export * from './deploy/actions'; +export { default as angularDeploy } from './deploy/actions'; export * from './deploy/builder'; + +// Schema and options types +export { Schema } from './deploy/schema'; +export { + GHPages, + PublishOptions, + DeployUser, + AngularOutputPath, + AngularOutputPathObject, + isOutputPathObject +} from './interfaces'; + +// Default configuration +export { defaults } from './engine/defaults'; + +// Core deployment engine +export { run as deployToGHPages } from './engine/engine'; + +// Advanced: Extracted option processing functions for custom workflows +export { + PreparedOptions, + setupMonkeypatch, + mapNegatedBooleans, + handleUserCredentials, + warnDeprecatedParameters, + appendCIMetadata, + injectTokenIntoRepoUrl +} from './engine/engine.prepare-options-helpers'; + +export { prepareOptions } from './engine/engine'; diff --git a/src/test-fixtures/angular-18.json b/src/test-fixtures/angular-18.json new file mode 100644 index 0000000..9016670 --- /dev/null +++ b/src/test-fixtures/angular-18.json @@ -0,0 +1,96 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "ng18": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/ng18", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kB", + "maximumError": "4kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "ng18:build:production" + }, + "development": { + "buildTarget": "ng18:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/src/test-fixtures/angular-19.json b/src/test-fixtures/angular-19.json new file mode 100644 index 0000000..129affd --- /dev/null +++ b/src/test-fixtures/angular-19.json @@ -0,0 +1,96 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "ng19": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/ng19", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "ng19:build:production" + }, + "development": { + "buildTarget": "ng19:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/src/test-fixtures/angular-20.json b/src/test-fixtures/angular-20.json new file mode 100644 index 0000000..8aad835 --- /dev/null +++ b/src/test-fixtures/angular-20.json @@ -0,0 +1,92 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "ng20": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "ng20:build:production" + }, + "development": { + "buildTarget": "ng20:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n" + }, + "test": { + "builder": "@angular/build:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + } + } + } + } + } +} diff --git a/src/test-fixtures/angular-21.json b/src/test-fixtures/angular-21.json new file mode 100644 index 0000000..e3d84bf --- /dev/null +++ b/src/test-fixtures/angular-21.json @@ -0,0 +1,73 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "ng21": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "ng21:build:production" + }, + "development": { + "buildTarget": "ng21:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/src/test-fixtures/angular-versions.spec.ts b/src/test-fixtures/angular-versions.spec.ts new file mode 100644 index 0000000..68a934b --- /dev/null +++ b/src/test-fixtures/angular-versions.spec.ts @@ -0,0 +1,116 @@ +/** + * Angular Version Compatibility Tests + * + * These tests verify that angular-cli-ghpages correctly handles the angular.json + * structure for each Angular version. The fixture files are actual angular.json + * files generated by `ng new` with each Angular CLI version. + * + * Key difference relevant to us: + * - Angular 18-19: outputPath is a string like "dist/" + * - Angular 20-21: outputPath is MISSING (uses default dist/) + */ + +import { SchematicContext, Tree } from '@angular-devkit/schematics'; +import fs from 'fs'; +import path from 'path'; +import { ngAdd } from '../ng-add'; + +// Load fixture files - actual angular.json from each Angular CLI version +const fixturesDir = path.join(__dirname); +const angular18Config = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'angular-18.json'), 'utf-8')); +const angular19Config = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'angular-19.json'), 'utf-8')); +const angular20Config = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'angular-20.json'), 'utf-8')); +const angular21Config = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'angular-21.json'), 'utf-8')); + +describe('Angular Version Compatibility', () => { + describe('Angular 18', () => { + it('has outputPath as string', () => { + const outputPath = angular18Config.projects.ng18.architect.build.options.outputPath; + expect(outputPath).toBe('dist/ng18'); + }); + + it('ng add succeeds', async () => { + const tree = Tree.empty(); + tree.create('angular.json', JSON.stringify(angular18Config)); + + const result = await ngAdd({ project: 'ng18' })(tree, {} as SchematicContext); + const resultConfig = JSON.parse(result.read('angular.json')!.toString()); + + expect(resultConfig.projects.ng18.architect.deploy.builder).toBe('angular-cli-ghpages:deploy'); + }); + }); + + describe('Angular 19', () => { + it('has outputPath as string', () => { + const outputPath = angular19Config.projects.ng19.architect.build.options.outputPath; + expect(outputPath).toBe('dist/ng19'); + }); + + it('ng add succeeds', async () => { + const tree = Tree.empty(); + tree.create('angular.json', JSON.stringify(angular19Config)); + + const result = await ngAdd({ project: 'ng19' })(tree, {} as SchematicContext); + const resultConfig = JSON.parse(result.read('angular.json')!.toString()); + + expect(resultConfig.projects.ng19.architect.deploy.builder).toBe('angular-cli-ghpages:deploy'); + }); + }); + + describe('Angular 20', () => { + it('has NO outputPath (uses default)', () => { + const options = angular20Config.projects.ng20.architect.build.options; + expect(options.outputPath).toBeUndefined(); + }); + + it('ng add succeeds despite missing outputPath', async () => { + const tree = Tree.empty(); + tree.create('angular.json', JSON.stringify(angular20Config)); + + const result = await ngAdd({ project: 'ng20' })(tree, {} as SchematicContext); + const resultConfig = JSON.parse(result.read('angular.json')!.toString()); + + expect(resultConfig.projects.ng20.architect.deploy.builder).toBe('angular-cli-ghpages:deploy'); + }); + }); + + describe('Angular 21', () => { + it('has NO outputPath (uses default)', () => { + const options = angular21Config.projects.ng21.architect.build.options; + expect(options.outputPath).toBeUndefined(); + }); + + it('ng add succeeds despite missing outputPath', async () => { + const tree = Tree.empty(); + tree.create('angular.json', JSON.stringify(angular21Config)); + + const result = await ngAdd({ project: 'ng21' })(tree, {} as SchematicContext); + const resultConfig = JSON.parse(result.read('angular.json')!.toString()); + + expect(resultConfig.projects.ng21.architect.deploy.builder).toBe('angular-cli-ghpages:deploy'); + }); + }); + + describe('Cross-version compatibility', () => { + it('all versions have projectType: application', () => { + expect(angular18Config.projects.ng18.projectType).toBe('application'); + expect(angular19Config.projects.ng19.projectType).toBe('application'); + expect(angular20Config.projects.ng20.projectType).toBe('application'); + expect(angular21Config.projects.ng21.projectType).toBe('application'); + }); + + it('all versions have build target', () => { + expect(angular18Config.projects.ng18.architect.build).toBeDefined(); + expect(angular19Config.projects.ng19.architect.build).toBeDefined(); + expect(angular20Config.projects.ng20.architect.build).toBeDefined(); + expect(angular21Config.projects.ng21.architect.build).toBeDefined(); + }); + + it('outputPath evolution: string β†’ string β†’ undefined β†’ undefined', () => { + expect(typeof angular18Config.projects.ng18.architect.build.options.outputPath).toBe('string'); + expect(typeof angular19Config.projects.ng19.architect.build.options.outputPath).toBe('string'); + expect(angular20Config.projects.ng20.architect.build.options.outputPath).toBeUndefined(); + expect(angular21Config.projects.ng21.architect.build.options.outputPath).toBeUndefined(); + }); + }); +}); diff --git a/src/test-prerequisites.spec.ts b/src/test-prerequisites.spec.ts new file mode 100644 index 0000000..c53ed56 --- /dev/null +++ b/src/test-prerequisites.spec.ts @@ -0,0 +1,156 @@ +import { execSync } from 'child_process'; + +/** + * TEST PREREQUISITES VALIDATION + * + * This test suite validates that the required development environment + * is correctly set up. These tests intentionally FAIL LOUDLY with clear + * instructions rather than skipping silently. + * + * angular-cli-ghpages is built on top of git. Without git, nothing works. + */ + +describe('Test Prerequisites', () => { + const projectRoot = process.cwd(); + + describe('Git availability', () => { + let gitVersion: string; + + it('must have git executable available on PATH', () => { + try { + gitVersion = execSync('git --version', { encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch (error) { + throw new Error( + '\n\n' + + '====================================================================\n' + + ' PREREQUISITE FAILURE: Git is not installed or not on PATH\n' + + '====================================================================\n' + + '\n' + + 'angular-cli-ghpages is built on top of git.\n' + + 'Tests cannot run without git being available.\n' + + '\n' + + 'To fix this:\n' + + '\n' + + ' macOS:\n' + + ' brew install git\n' + + ' # or install Xcode Command Line Tools:\n' + + ' xcode-select --install\n' + + '\n' + + ' Ubuntu/Debian:\n' + + ' sudo apt-get update\n' + + ' sudo apt-get install git\n' + + '\n' + + ' Windows:\n' + + ' Download from https://git-scm.com/download/win\n' + + ' # or use winget:\n' + + ' winget install Git.Git\n' + + '\n' + + ' Verify installation:\n' + + ' git --version\n' + + '\n' + + '====================================================================\n' + ); + } + + // If we got here, git is available + expect(gitVersion).toMatch(/^git version/); + }); + + it('must be running in a git repository', () => { + // Only run if git is available (previous test passed) + let gitDir: string; + + try { + gitDir = execSync('git rev-parse --git-dir', { + encoding: 'utf8', + stdio: 'pipe', + cwd: projectRoot + }).trim(); + } catch (error) { + throw new Error( + '\n\n' + + '====================================================================\n' + + ' PREREQUISITE FAILURE: Not running in a git repository\n' + + '====================================================================\n' + + '\n' + + 'angular-cli-ghpages tests must run within a git repository.\n' + + '\n' + + 'Current directory:\n' + + ` ${projectRoot}\n` + + '\n' + + 'To fix this:\n' + + '\n' + + ' If you cloned this repository:\n' + + ' # Ensure you are in the correct directory\n' + + ` cd ${projectRoot}\n` + + ' git status\n' + + '\n' + + ' If you downloaded as ZIP:\n' + + ' # Clone the repository instead:\n' + + ' git clone https://github.com/angular-schule/angular-cli-ghpages.git\n' + + ' cd angular-cli-ghpages\n' + + '\n' + + ' If git was initialized but corrupted:\n' + + ' # Check if .git directory exists:\n' + + ' ls -la .git\n' + + ' # If corrupted, re-clone the repository\n' + + '\n' + + '====================================================================\n' + ); + } + + // If we got here, we are in a git repository + expect(gitDir.length).toBeGreaterThan(0); + }); + + it('must have origin remote configured', () => { + // Only run if git is available and we are in a repo (previous tests passed) + let remoteUrl: string; + + try { + remoteUrl = execSync('git config --get remote.origin.url', { + encoding: 'utf8', + stdio: 'pipe', + cwd: projectRoot + }).trim(); + } catch (error) { + throw new Error( + '\n\n' + + '====================================================================\n' + + ' PREREQUISITE FAILURE: No origin remote configured\n' + + '====================================================================\n' + + '\n' + + 'angular-cli-ghpages tests expect a git origin remote to be configured.\n' + + '\n' + + 'To fix this:\n' + + '\n' + + ' Add the origin remote:\n' + + ' git remote add origin https://github.com/angular-schule/angular-cli-ghpages.git\n' + + '\n' + + ' Verify it was added:\n' + + ' git remote -v\n' + + '\n' + + ' If origin exists but is misconfigured:\n' + + ' # View current remotes:\n' + + ' git remote -v\n' + + '\n' + + ' # Update origin URL:\n' + + ' git remote set-url origin https://github.com/angular-schule/angular-cli-ghpages.git\n' + + '\n' + + ' Expected origin:\n' + + ' https://github.com/angular-schule/angular-cli-ghpages.git\n' + + ' or\n' + + ' git@github.com:angular-schule/angular-cli-ghpages.git\n' + + '\n' + + '====================================================================\n' + ); + } + + // If we got here, origin is configured + expect(remoteUrl.length).toBeGreaterThan(0); + // Verify it looks like a valid git URL (either HTTPS or SSH) + expect(remoteUrl).toMatch(/^(https?:\/\/|git@)/); + }); + }); + +}); diff --git a/src/tsconfig.build.json b/src/tsconfig.build.json index c180ac8..7b351d0 100644 --- a/src/tsconfig.build.json +++ b/src/tsconfig.build.json @@ -1,4 +1,7 @@ { "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true + }, "exclude": ["node_modules", "test", "**/*spec.ts"] } diff --git a/src/tsconfig.json b/src/tsconfig.json index 0b7ed27..d60cb72 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,7 +5,7 @@ "emitDecoratorMetadata": true, "module": "commonjs", "target": "es2015", - "noImplicitAny": false, + "noImplicitAny": true, "outDir": "dist", "rootDir": ".", "sourceMap": true, @@ -29,6 +29,5 @@ "strictMetadataEmit": true, "enableSummariesForJit": false }, - "exclude": ["node_modules", "dist"], - "typeRoots": ["node_modules/@types"] + "exclude": ["node_modules", "dist"] }