diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..dbfb5c068b --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Environment used to build and release the CLI. +# +# Copy to .env and fill in what you need; .env is gitignored, .env.example is not. +# The build scripts load .env automatically, but a value already exported in the +# environment always wins - CI secrets are never shadowed by a stray local file. + + +# --- Analytics ------------------------------------------------------------- +# Measurement Protocol api secret, paired with the measurement id in +# scripts/set-ga-id.js. Create one under +# GA4 Admin -> Data Streams -> -> Measurement Protocol API secrets +# +# Deliberately not committed: this repository is public, so a committed secret +# would make every contributor's `npm run pack.release` report into the +# production property. Leave it unset locally - builds then report nothing. +# +# Set as the GA_API_SECRET repository secret for releases. +GA_API_SECRET= + +# Same, for the dev measurement id. Unused until a dev id is set in +# scripts/set-ga-id.js. +GA_API_SECRET_DEV= + + +# --- Publishing ------------------------------------------------------------ +# Only needed when the USE_NPM_TOKEN repository variable is "true". The default +# path is OIDC trusted publishing, which needs no token at all. +NPM_PUBLISH_TOKEN= + +# Used by the OpenSSF Scorecard workflow to read branch protection rules. +SCORECARD_TOKEN= + +# GITHUB_TOKEN is provided by Actions automatically and never needs setting here. diff --git a/.github/workflows/codeql-advanced.yml b/.github/workflows/codeql-advanced.yml index 8e441dffc2..3a37f9baa4 100644 --- a/.github/workflows/codeql-advanced.yml +++ b/.github/workflows/codeql-advanced.yml @@ -60,7 +60,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` @@ -70,7 +70,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -98,6 +98,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4a514339ef..181ab43c29 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.3.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4.3.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/npm_release_cli.yml b/.github/workflows/npm_release_cli.yml index 13526d53b3..4354d9fa3b 100644 --- a/.github/workflows/npm_release_cli.yml +++ b/.github/workflows/npm_release_cli.yml @@ -11,15 +11,16 @@ on: workflow_dispatch: inputs: release_type: - description: 'Release type. "dev" publishes a -next prerelease without bumping package.json. patch/minor/major/prerelease bump package.json, commit + tag to main, then publish as a stable release.' - type: choice - options: - - dev - - patch - - minor - - major - - prerelease - default: patch + description: >- + Release to cut. Leave empty for a rolling "next" prerelease (no version + bump). "dev" publishes a -dev prerelease (no bump). A semver keyword + (patch/minor/major) or an explicit version (e.g. 9.1.0, 9.1.0-alpha.1) + bumps package.json, commits + tags v, then publishes a stable + release. A prerelease version publishes under the dist-tag matching its + prerelease id (alpha/beta/rc); a plain version publishes under "latest". + type: string + required: false + default: '' permissions: read-all @@ -39,16 +40,16 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 registry-url: "https://registry.npmjs.org" @@ -62,17 +63,24 @@ jobs: echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV - name: Bump, commit and tag stable release (manual dispatch) - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type != 'dev' }} + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type != '' && inputs.release_type != 'dev' }} + env: + # env indirection keeps the free-text dispatch input out of shell interpolation + RELEASE_INPUT: ${{ inputs.release_type }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - npm version ${{ inputs.release_type }} -m "chore: release v%s" + # npm version accepts a semver keyword (patch/minor/major) or an explicit + # version; strip an optional leading "v" so v9.1.0 and 9.1.0 both work. + npm version "${RELEASE_INPUT#v}" -m "chore: release v%s" NPM_VERSION=$(node -e "console.log(require('./package.json').version);") echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV git push origin HEAD:${GITHUB_REF_NAME} --follow-tags - name: Bump version for dev release - if: ${{ !contains(github.ref, 'refs/tags/') && (github.event_name != 'workflow_dispatch' || inputs.release_type == 'dev') }} + if: ${{ !contains(github.ref, 'refs/tags/') && (github.event_name != 'workflow_dispatch' || inputs.release_type == '' || inputs.release_type == 'dev') }} + env: + NPM_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type == 'dev' && 'dev' || 'next' }} run: | NPM_VERSION=$(node ./scripts/get-next-version.js) echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV @@ -80,9 +88,14 @@ jobs: - name: Output NPM Version and tag id: npm_version_output + env: + # true only for a manual dispatch that cut a real (bumped) release — not + # the empty "next" build or the "dev" channel. Computed in the GitHub + # expression context so the free-text input never reaches the shell. + IS_DISPATCH_RELEASE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type != '' && inputs.release_type != 'dev' }} run: | NPM_TAG=$(node ./scripts/get-npm-tag.js) - if [[ "${GITHUB_REF}" == refs/tags/* ]] || [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.release_type }}" != "dev" ]]; then + if [[ "${GITHUB_REF}" == refs/tags/* ]] || [[ "$IS_DISPATCH_RELEASE" == "true" ]]; then IS_RELEASE=true else IS_RELEASE=false @@ -91,8 +104,18 @@ jobs: echo NPM_TAG=$NPM_TAG >> $GITHUB_OUTPUT echo IS_RELEASE=$IS_RELEASE >> $GITHUB_OUTPUT + - name: Check analytics is configured + env: + GA_API_SECRET: ${{ secrets.GA_API_SECRET }} + run: | + if [ -z "$GA_API_SECRET" ]; then + echo "::warning::GA_API_SECRET is not set, so this release reports no analytics." + fi + - name: Build nativescript - run: npm pack + env: + GA_API_SECRET: ${{ secrets.GA_API_SECRET }} + run: npm run pack.release - name: Upload npm package artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -113,11 +136,11 @@ jobs: NPM_TAG: ${{needs.build.outputs.npm_tag}} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 registry-url: "https://registry.npmjs.org" @@ -164,16 +187,16 @@ jobs: NPM_VERSION: ${{needs.build.outputs.npm_version}} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: v${{needs.build.outputs.npm_version}} - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 diff --git a/.github/workflows/npm_release_doctor.yml b/.github/workflows/npm_release_doctor.yml index 8366bc9d47..2c15546732 100644 --- a/.github/workflows/npm_release_doctor.yml +++ b/.github/workflows/npm_release_doctor.yml @@ -3,50 +3,238 @@ name: '@nativescript/doctor -> npm' on: push: branches: [ 'main' ] + tags: + - '@nativescript/doctor@*' paths: - 'packages/doctor/**' workflow_dispatch: + inputs: + release_type: + description: >- + Release to cut. Leave empty for a rolling "next" prerelease (no version + bump). "dev" publishes a -dev prerelease (no bump). A semver keyword + (patch/minor/major) or an explicit version (e.g. 2.1.0, 2.1.0-alpha.1) + bumps packages/doctor/package.json, commits + tags + @nativescript/doctor@, then publishes a stable release. A + prerelease version publishes under the dist-tag matching its prerelease + id (alpha/beta/rc); a plain version publishes under "latest". + type: string + required: false + default: '' -defaults: - run: - working-directory: packages/doctor +permissions: read-all env: NPM_TAG: 'next' -permissions: - contents: read - jobs: - release: + build: + name: Build runs-on: ubuntu-latest + permissions: + contents: write + defaults: + run: + working-directory: packages/doctor + outputs: + npm_version: ${{ steps.npm_version_output.outputs.NPM_VERSION }} + npm_tag: ${{ steps.npm_version_output.outputs.NPM_TAG }} + is_release: ${{ steps.npm_version_output.outputs.IS_RELEASE }} steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.14.0 + registry-url: "https://registry.npmjs.org" - name: Setup run: npm install - - name: Generate Version + - name: Get Current Version run: | - echo NPM_VERSION=$(node -e "console.log(require('./package.json').version);")-$NPM_TAG-$(date +"%m-%d-%Y")-$GITHUB_RUN_ID >> $GITHUB_ENV + NPM_VERSION=$(node -e "console.log(require('./package.json').version);") + echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV - - name: Bump Version - run: npm --no-git-tag-version version $NPM_VERSION + - name: Bump, commit and tag stable release (manual dispatch) + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type != '' && inputs.release_type != 'dev' }} + env: + # env indirection keeps the free-text dispatch input out of shell interpolation + RELEASE_INPUT: ${{ inputs.release_type }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # npm version accepts a semver keyword (patch/minor/major) or an explicit + # version; strip an optional leading "v" so v2.1.0 and 2.1.0 both work. + # The tag is written by hand: npm's own v tag namespace belongs + # to the CLI package at the repo root. + npm version "${RELEASE_INPUT#v}" --no-git-tag-version + NPM_VERSION=$(node -e "console.log(require('./package.json').version);") + echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV + git add package.json package-lock.json + git commit -m "chore(doctor): release @nativescript/doctor@$NPM_VERSION" + git tag -a "@nativescript/doctor@$NPM_VERSION" -m "@nativescript/doctor@$NPM_VERSION" + git push origin HEAD:${GITHUB_REF_NAME} --follow-tags + + - name: Bump version for dev release + if: ${{ !contains(github.ref, 'refs/tags/') && (github.event_name != 'workflow_dispatch' || inputs.release_type == '' || inputs.release_type == 'dev') }} + env: + NPM_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type == 'dev' && 'dev' || 'next' }} + # the shared version scripts live at the repo root, whose dependencies + # are not installed here — semver resolves out of this package instead + NODE_PATH: ${{ github.workspace }}/packages/doctor/node_modules + run: | + LAST_TAG=$(git describe --tags --abbrev=0 --match='@nativescript/doctor@*' 2>/dev/null || true) + LAST_TAGGED_VERSION=${LAST_TAG##*@} + # before the first scoped tag exists, the published version in + # package.json is the only baseline to bump past + export LAST_TAGGED_VERSION=${LAST_TAGGED_VERSION:-$NPM_VERSION} + NPM_VERSION=$(node "$GITHUB_WORKSPACE/scripts/get-next-version.js") + echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV + npm version $NPM_VERSION --no-git-tag-version + + - name: Output NPM Version and tag + id: npm_version_output + env: + # true only for a manual dispatch that cut a real (bumped) release — not + # the empty "next" build or the "dev" channel. Computed in the GitHub + # expression context so the free-text input never reaches the shell. + IS_DISPATCH_RELEASE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type != '' && inputs.release_type != 'dev' }} + NODE_PATH: ${{ github.workspace }}/packages/doctor/node_modules + run: | + NPM_TAG=$(node "$GITHUB_WORKSPACE/scripts/get-npm-tag.js") + if [[ "${GITHUB_REF}" == refs/tags/* ]] || [[ "$IS_DISPATCH_RELEASE" == "true" ]]; then + IS_RELEASE=true + else + IS_RELEASE=false + fi + echo NPM_VERSION=$NPM_VERSION >> $GITHUB_OUTPUT + echo NPM_TAG=$NPM_TAG >> $GITHUB_OUTPUT + echo IS_RELEASE=$IS_RELEASE >> $GITHUB_OUTPUT - name: Build @nativescript/doctor run: npm pack - - name: Publish @nativescript/doctor + - name: Upload npm package artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: npm-package-doctor + path: packages/doctor/nativescript-doctor-${{steps.npm_version_output.outputs.NPM_VERSION}}.tgz + + publish: + runs-on: ubuntu-latest + environment: npm-publish + needs: + - build + permissions: + contents: read + id-token: write + env: + NPM_VERSION: ${{needs.build.outputs.npm_version}} + NPM_TAG: ${{needs.build.outputs.npm_tag}} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.14.0 + registry-url: "https://registry.npmjs.org" + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: npm-package-doctor + path: dist + + - name: Update npm (required for OIDC trusted publishing) + run: | + npm install -g npm@^11.5.1 + npm --version + + - name: Publish package (OIDC trusted publishing) + if: ${{ vars.USE_NPM_TOKEN != 'true' }} + run: | + echo "Publishing @nativescript/doctor@$NPM_VERSION to NPM with tag $NPM_TAG via OIDC trusted publishing..." + unset NODE_AUTH_TOKEN + if [ -n "${NPM_CONFIG_USERCONFIG:-}" ]; then + rm -f "$NPM_CONFIG_USERCONFIG" + fi + npm publish ./dist/nativescript-doctor-${{env.NPM_VERSION}}.tgz --tag $NPM_TAG --access public --provenance env: - NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + NODE_AUTH_TOKEN: "" + + - name: Publish package (granular token) + if: ${{ vars.USE_NPM_TOKEN == 'true' }} run: | - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc - echo "Publishing @nativescript/doctor@$NPM_VERSION to NPM with tag $NPM_TAG..." - npm publish nativescript-doctor-$NPM_VERSION.tgz --tag $NPM_TAG + echo "Publishing @nativescript/doctor@$NPM_VERSION to NPM with tag $NPM_TAG via granular token..." + npm publish ./dist/nativescript-doctor-${{env.NPM_VERSION}}.tgz --tag $NPM_TAG --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + + github-release: + runs-on: ubuntu-latest + # runs for tag pushes and for manual dispatches that bumped a stable release + if: ${{ needs.build.outputs.is_release == 'true' }} + permissions: + contents: write + needs: + - build + env: + NPM_VERSION: ${{needs.build.outputs.npm_version}} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: 'refs/tags/@nativescript/doctor@${{needs.build.outputs.npm_version}}' + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.14.0 + + - name: Setup + working-directory: packages/doctor + run: npm i --ignore-scripts --no-package-lock + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: npm-package-doctor + path: dist + + - name: Generate provenance statement + run: | + TGZ_PATH=$(ls dist/nativescript-doctor-*.tgz | head -n1) + TGZ_NAME=$(basename "$TGZ_PATH") + TGZ_SHA=$(sha256sum "$TGZ_PATH" | awk '{ print $1 }') + PROV_PATH="dist/${TGZ_NAME%.tgz}.intoto.jsonl" + + cat > "$PROV_PATH" < "$GITHUB_WORKSPACE/body.md" + + - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + with: + tag: '@nativescript/doctor@${{needs.build.outputs.npm_version}}' + artifacts: "dist/nativescript-doctor-*.tgz,dist/nativescript-doctor-*.intoto.jsonl" + bodyFile: "body.md" + prerelease: ${{needs.build.outputs.npm_tag != 'latest'}} + allowUpdates: true diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 58910b421f..768218fc1d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,20 +33,19 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.3.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4.3.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # (Optional) PAT token. Add `repo_token: ${{ secrets.SCORECARD_TOKEN }}` if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecards on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. - repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers @@ -68,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: - sarif_file: results.sarif \ No newline at end of file + sarif_file: results.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..f08d69c90d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,93 @@ +name: 'Tests' + +on: + pull_request: + push: + branches: [ 'main' ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test (${{ matrix.os }}, Node ${{ matrix.node-version }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + node-version: [ '20.x', '22.x', '24.x' ] + include: + # A darwin leg is what covers the iOS-specific suites - they are gated + # on the platform and never execute anywhere else. One version is + # enough for that; 24 is the active LTS. + - os: macos-latest + node-version: '24.x' + + steps: + + - name: Harden the runner (Audit all outbound calls) + # only supported on the Ubuntu runners + if: runner.os == 'Linux' + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # nothing here needs git auth after checkout, and npm test runs + # repository code + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install + run: npm ci --ignore-scripts + + - name: Test + run: npm test + + doctor: + name: Doctor (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # No darwin leg: unlike the CLI suites, these stub the platform through + # a fake HostInfo rather than probing it, so a second OS runs identical + # assertions. + node-version: [ '20.x', '22.x', '24.x' ] + + steps: + + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: packages/doctor/package-lock.json + + - name: Install + working-directory: packages/doctor + run: npm ci + + - name: Test + working-directory: packages/doctor + run: npm test diff --git a/.gitignore b/.gitignore index 0fd3dda24f..96e627f0c8 100644 --- a/.gitignore +++ b/.gitignore @@ -87,5 +87,18 @@ lib/common/test-reports.xml !lib/common/test-scripts/** !lib/common/scripts/** config/test-deps-versions-generated.json -!scripts/get-next-version.js -!scripts/get-npm-tag.js \ No newline at end of file +!scripts/*.js +!dev/*.js + +# build output +/dist + +# local secrets, filled in from .env.example +.env +.env.* +!.env.example + +# Claude Code local state - worktrees and per-user settings. Ignoring the +# contents rather than the directory keeps shared settings.json committable. +.claude/* +!.claude/settings.json diff --git a/BuildPackage.cmd b/BuildPackage.cmd deleted file mode 100644 index 03ed3ba392..0000000000 --- a/BuildPackage.cmd +++ /dev/null @@ -1,12 +0,0 @@ -call "c:\Program Files (x86)\nodejs\nodevars.bat" -call npm.cmd install -g grunt-cli - -set NATIVESCRIPT_SKIP_POSTINSTALL_TASKS=1 -call grunt.cmd enableScripts:false -call npm.cmd install -call grunt.cmd enableScripts:true -set NATIVESCRIPT_SKIP_POSTINSTALL_TASKS= - -call grunt.cmd pack --no-color - -call npm.cmd cache rm nativescript diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cc5756fd9..c850110d71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ Before you submit a Pull Request, consider the following guidelines. ```bash git clone git@github.com:NativeScript/nativescript-cli.git ``` -* Run the setup script. This will initialize the git submodule, install the node dependencies and build with grunt. +* Run the setup script. This will install the node dependencies and set up the git hooks. ```bash npm run setup ``` @@ -44,15 +44,15 @@ Before you submit a Pull Request, consider the following guidelines. * Create your patch and include appropriate test cases. * Build your changes locally. ```bash - ./node_modules/.bin/grunt + npm run build ``` * Ensure all the tests pass. ```bash - ./node_modules/.bin/grunt test + npm test ``` -* Ensure that your code passes the linter. +* Ensure that your code is formatted. ```bash - ./node_modules/.bin/grunt lint + npm run prettier ``` * Commit your changes following the [commit message guidelines](https://github.com/NativeScript/NativeScript/blob/master/CONTRIBUTING.md#-commit-message-guidelines) (the commit message is used to generate release notes). ```bash diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index a08b50c319..0000000000 --- a/Gruntfile.js +++ /dev/null @@ -1,297 +0,0 @@ -const childProcess = require("child_process"); -const EOL = require("os").EOL; -const path = require("path"); -const now = new Date().toISOString(); -const manifest = require('pacote').manifest; - - -const ENVIRONMENTS = { - live: "live", - dev: "dev" -}; - -const GA_TRACKING_IDS = { - [ENVIRONMENTS.dev]: "UA-111455-51", - [ENVIRONMENTS.live]: "UA-111455-44" -}; - -const CONFIG_DATA = { - filePath: "config/config.json", - gaKey: "GA_TRACKING_ID" -} - -function shallowCopy(obj) { - var result = {}; - Object.keys(obj).forEach(function (key) { - result[key] = obj[key]; - }); - return result; -} - -var travis = process.env["TRAVIS"]; -var buildNumber = process.env["PACKAGE_VERSION"] || process.env["BUILD_NUMBER"] || "non-ci"; - -module.exports = function (grunt) { - grunt.initConfig({ - copyPackageTo: process.env["CopyPackageTo"] || ".", - - jobName: travis ? "travis" : (process.env["JOB_NAME"] || "local"), - buildNumber: buildNumber, - dateString: now.substr(0, now.indexOf("T")), - - pkg: grunt.file.readJSON("package.json"), - ts: { - options: grunt.file.readJSON("tsconfig.json").compilerOptions, - - devlib: { - src: ["lib/**/*.ts", "!lib/common/node_modules/**/*.ts"], - reference: "lib/.d.ts" - }, - - devall: { - src: ["lib/**/*.ts", "test/**/*.ts", "!lib/common/node_modules/**/*.ts", "lib/common/test/unit-tests/**/*.ts", "definitions/**/*.ts", "!lib/common/test/.d.ts"], - reference: "lib/.d.ts" - }, - - release_build: { - src: ["lib/**/*.ts", "test/**/*.ts", "!lib/common/node_modules/**/*.ts"], - reference: "lib/.d.ts", - options: { - sourceMap: false, - removeComments: true - } - }, - }, - - watch: { - devall: { - files: ["lib/**/*.ts", 'test/**/*.ts', "!lib/common/node_modules/**/*.ts", "!lib/common/messages/**/*.ts"], - tasks: [ - 'ts:devall', - 'shell:npm_test' - ], - options: { - atBegin: true, - interrupt: true - } - }, - ts: { - files: ["lib/**/*.ts", 'test/**/*.ts', "!lib/common/node_modules/**/*.ts"], - tasks: [ - 'ts:devall' - ], - options: { - atBegin: true, - interrupt: true - } - } - }, - - shell: { - options: { - stdout: true, - stderr: true, - failOnError: true - }, - - build_package: { - command: "npm pack", - options: { - execOptions: { - env: (function () { - var env = shallowCopy(process.env); - env["NATIVESCRIPT_SKIP_POSTINSTALL_TASKS"] = "1"; - return env; - })() - } - } - }, - - npm_test: { - command: "npm test" - } - - }, - - clean: { - src: ["test/**/*.js*", - "!test/files/**/*.js*", - "lib/**/*.js*", - "!test-scripts/**/*", - "!lib/common/vendor/*.js", - "!lib/common/**/*.json", - "!lib/common/Gruntfile.js", - "!lib/common/node_modules/**/*", - "!lib/common/hooks/**/*.js", - "!lib/common/bin/*.js", - "!lib/common/test-scripts/**/*", - "!lib/common/scripts/**/*", - "!lib/common/test/resources/**/*", - "*.tgz"] - }, - template: { - 'process-markdowns': { - options: { - data: { - "isJekyll": true, - "isHtml": true, - "isConsole": true, - "isWindows": true, - "isMacOS": true, - "isLinux": true, - "constants": "" - } - }, - files: [{ - expand: true, - cwd: "docs/man_pages/", - src: "**/*.md", - dest: "docs-cli/", - ext: ".md" - }] - } - } - }); - - grunt.loadNpmTasks("grunt-contrib-clean"); - grunt.loadNpmTasks("grunt-contrib-copy"); - grunt.loadNpmTasks("grunt-contrib-watch"); - grunt.loadNpmTasks("grunt-shell"); - grunt.loadNpmTasks("grunt-ts"); - grunt.loadNpmTasks("grunt-template"); - - grunt.registerTask("set_package_version", function (version) { - // NOTE: DO NOT call this task in npm's prepack script - it will change the version in package.json, - // but npm will still try to publish the version that was originally specified in the package.json/ - // Also this may break some Jenkins builds as the produced package will have unexpected name. - var buildVersion = version !== undefined ? version : buildNumber; - if (process.env["BUILD_CAUSE_GHPRBCAUSE"]) { - buildVersion = "PR" + buildVersion; - } - - var packageJson = grunt.file.readJSON("package.json"); - var versionParts = packageJson.version.split("-"); - - // The env is used in Jenkins job to produce package that will be releasd with "latest" tag in npm (i.e. strict version). - if (!process.env["RELEASE_BUILD"]) { - versionParts[1] = buildVersion; - packageJson.version = versionParts.join("-"); - } - - grunt.file.write("package.json", JSON.stringify(packageJson, null, " ")); - }); - - grunt.registerTask("tslint:build", function (version) { - childProcess.execSync("npm run tslint", { stdio: "inherit" }); - }); - - const setConfig = (key, value) => { - const configJson = grunt.file.readJSON(CONFIG_DATA.filePath); - configJson[key] = value; - const stringConfigContent = JSON.stringify(configJson, null, " ") + EOL; - grunt.file.write(CONFIG_DATA.filePath, stringConfigContent); - } - - grunt.registerTask("set_live_ga_id", function () { - setConfig(CONFIG_DATA.gaKey, GA_TRACKING_IDS[ENVIRONMENTS.live]); - }); - - grunt.registerTask("set_dev_ga_id", function () { - setConfig(CONFIG_DATA.gaKey, GA_TRACKING_IDS[ENVIRONMENTS.dev]); - }); - - grunt.registerTask("verify_live_ga_id", function () { - var configJson = grunt.file.readJSON(CONFIG_DATA.filePath); - - if (configJson[CONFIG_DATA.gaKey] !== GA_TRACKING_IDS[ENVIRONMENTS.live]) { - throw new Error("Google Analytics id is not configured correctly."); - } - }); - - grunt.registerTask("test", ["ts:devall", "shell:npm_test"]); - - registerTestingDependenciesTasks(grunt); - - grunt.registerTask("prepare", [ - "clean", - "ts:release_build", - "generate_unit_testing_dependencies", - "verify_unit_testing_dependencies", - "shell:npm_test", - - "set_live_ga_id", - "verify_live_ga_id" - ]); - grunt.registerTask("pack", [ - "set_package_version", - "shell:build_package" - ]); - - grunt.registerTask("travisPack", function () { - if (travis && process.env.TRAVIS_PULL_REQUEST_BRANCH) { - return grunt.task.run("pack"); - } - - // Set correct version in Travis job, so the deploy will not publish strict version (for example 5.2.0). - grunt.task.run("set_package_version"); - console.log(`Skipping pack step as the current build is not from PR, so it will be packed from the deploy provider.`); - }); - grunt.registerTask("lint", ["tslint:build"]); - grunt.registerTask("all", ["clean", "test", "lint"]); - grunt.registerTask("rebuild", ["clean", "default"]); - grunt.registerTask("default", ["ts:devlib", "generate_unit_testing_dependencies"]); - grunt.registerTask("docs-jekyll", ['template']); -}; - -function registerTestingDependenciesTasks(grunt) { - const configsBasePath = path.join(__dirname, "config"); - const generatedVersionFilePath = path.join(configsBasePath, "test-deps-versions-generated.json"); - - grunt.registerTask("generate_unit_testing_dependencies", async function () { - const done = this.async(); - - const dependenciesVersions = {}; - let testDependencies; - - try { - testDependencies = grunt.file.readJSON(path.join(configsBasePath, "test-dependencies.json")); - } catch (err) { - grunt.log.error("Could not read test-dependencies.json:", err); - return done(false); - } - - (async () => { - try { - for (const dep of testDependencies) { - if (dep.version) { - dependenciesVersions[dep.name] = dep.version; - } else { - dependenciesVersions[dep.name] = await latestVersion(dep.name); - } - } - grunt.file.write( - generatedVersionFilePath, - JSON.stringify(dependenciesVersions, null, 2) - ); - grunt.log.writeln("Wrote", generatedVersionFilePath); - done(); - } catch (err) { - grunt.log.error(err); - done(false); - } - })(); - }); - - grunt.registerTask("verify_unit_testing_dependencies", function () { - if (!grunt.file.exists(generatedVersionFilePath)) { - throw new Error("Unit testing dependencies are not configured."); - } - }); -} - -async function latestVersion(name) { - // only fetches the package.json for the latest dist-tag - const { version } = await manifest(name.toLowerCase(), { fullMetadata: false }); - return version; -} - diff --git a/README.md b/README.md index 7324ffcff2..67ae265a1c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ npm version license downloads +inspect.software score badge for NativeScript/nativescript-cli

Get it using: `npm install -g nativescript` diff --git a/bin/tns b/bin/tns index fb5b323617..b66857aa8b 100755 --- a/bin/tns +++ b/bin/tns @@ -2,11 +2,75 @@ "use strict"; var path = require("path"), - pathToLib = path.join(__dirname, "..", "lib"), + fs = require("fs"), + // In the published package dist/ is the root, so lib/ already sits next to + // bin/. In a source checkout lib/ holds only TypeScript and the compiled + // output lives under dist/. + distLib = path.join(__dirname, "..", "dist", "lib"), + pathToLib = fs.existsSync(distLib) + ? distLib + : path.join(__dirname, "..", "lib"), pathToCommon = path.join(pathToLib, "common"); require(path.join(pathToCommon, "verify-node-version")).verifyNodeVersion(); +// Prefer a project-local CLI install when one is resolvable from the current +// directory: the project pins its own nativescript version, and running the +// invoked (typically global) copy against it means two CLI versions +// disagreeing about one project. The probe must never break the CLI - any +// failure falls through to the invoked copy. Opt out per invocation with +// --no-local-cli or NS_CLI_NO_LOCAL=1; NS_CLI_LOCAL_DELEGATED marks the +// handed-off process so the local copy never delegates again. +var noLocalFlagIndex = process.argv.indexOf("--no-local-cli"); +if (noLocalFlagIndex !== -1) { + process.argv.splice(noLocalFlagIndex, 1); +} + +if ( + !process.env.NS_CLI_LOCAL_DELEGATED && + !process.env.NS_CLI_NO_LOCAL && + noLocalFlagIndex === -1 +) { + var localEntry = null; + var localVersion = null; + var localDir = null; + try { + var localPackageJsonPath = require.resolve("nativescript/package.json", { + paths: [process.cwd()], + }); + var ownPackageJsonPath = path.join(__dirname, "..", "package.json"); + // realpath both sides so an npm-linked or symlinked install of the + // same copy is not mistaken for a different one. + if ( + fs.realpathSync(localPackageJsonPath) !== + fs.realpathSync(ownPackageJsonPath) + ) { + localDir = path.dirname(localPackageJsonPath); + var candidate = path.join(localDir, "bin", "tns"); + if (fs.existsSync(candidate)) { + localEntry = candidate; + localVersion = require(localPackageJsonPath).version; + } + } + } catch (err) { + // No local install resolvable from cwd - run the invoked copy. + } + + if (localEntry) { + process.env.NS_CLI_LOCAL_DELEGATED = "1"; + // stderr, so scripts parsing stdout (e.g. `ns --version`) are unaffected. + console.error( + "Using the project-local nativescript@" + + localVersion + + " (" + + localDir + + ").", + ); + require(localEntry); + return; + } +} + var pathToCliExecutable = path.join(pathToLib, "nativescript-cli.js"); require(pathToCliExecutable); diff --git a/config/config.json b/config/config.json index 1872961f1d..e84563c574 100644 --- a/config/config.json +++ b/config/config.json @@ -4,5 +4,6 @@ "ANDROID_DEBUG_UI_MAC": "Google Chrome", "USE_POD_SANDBOX": false, "DISABLE_HOOKS": false, - "GA_TRACKING_ID": "UA-111455-51" + "GA_MEASUREMENT_ID": "", + "GA_API_SECRET": "" } diff --git a/config/test-dependencies.json b/config/test-dependencies.json index 343b1c22fa..6a2ff78a7d 100644 --- a/config/test-dependencies.json +++ b/config/test-dependencies.json @@ -1,15 +1,40 @@ [ { - "name": "@jsdevtools/coverage-istanbul-loader" + "name": "vitest", + "framework": "vitest", + "version": "~4.1.10" }, { - "name": "karma" + "name": "@vitest/runner", + "framework": "vitest", + "version": "~4.1.10" }, { - "name": "karma-coverage" + "name": "@nativescript/unit-test-runner", + "framework": "vitest", + "version": "^5.0.0-alpha.0" }, { - "name": "karma-nativescript-launcher" + "name": "@valor/nativescript-websockets", + "framework": "vitest", + "version": "^2.0.3", + "saveInDependencies": true + }, + { + "name": "@jsdevtools/coverage-istanbul-loader", + "frameworks": ["jasmine", "mocha", "qunit"] + }, + { + "name": "karma", + "frameworks": ["jasmine", "mocha", "qunit"] + }, + { + "name": "karma-coverage", + "frameworks": ["jasmine", "mocha", "qunit"] + }, + { + "name": "karma-nativescript-launcher", + "frameworks": ["jasmine", "mocha", "qunit"] }, { "name": "mocha", @@ -53,6 +78,7 @@ "projectType": ".ts" }, { - "name": "nyc" + "name": "nyc", + "frameworks": ["jasmine", "mocha", "qunit"] } -] \ No newline at end of file +] diff --git a/contracts/package.json b/contracts/package.json new file mode 100644 index 0000000000..4c4d479642 --- /dev/null +++ b/contracts/package.json @@ -0,0 +1,4 @@ +{ + "main": "../lib/contracts/index.js", + "types": "../lib/contracts/index.d.ts" +} diff --git a/defining-commands.md b/defining-commands.md new file mode 100644 index 0000000000..ef7fc024c1 --- /dev/null +++ b/defining-commands.md @@ -0,0 +1,352 @@ +Defining Commands +================= + +`defineCommand` is the declarative way to add a command to the NativeScript +CLI. A definition is a plain object: a name, an option schema, and a `run` +function. The CLI compiles it into the command shape its registry expects, so a +definition gets the same option parsing, hooks, analytics and help wiring as a +hand-written command class — without a class, a constructor, or an +`allowedParameters` array. + +This is purely additive. The legacy `ICommand` classes registered through +`$injector.registerCommand` keep working exactly as before, and the two styles +coexist in the same registry. + +At a glance +----------- + +```ts +import { + defineCommand, + booleanOption, + stringOption, +} from "nativescript/contracts"; + +export default defineCommand({ + name: "widget|add", + description: "Adds a widget to the project", + options: { + overwrite: booleanOption({ default: false }), + output: stringOption({ alias: "o" }), + }, + arguments: "any", + async run(ctx) { + // ctx.args -> string[] of positional arguments + // ctx.options -> { overwrite: boolean; output: string | undefined } + if (ctx.options.output) { + console.log(`adding ${ctx.args.join(", ")} to ${ctx.options.output}`); + } + }, +}); +``` + +`defineCommand` validates the definition and returns it, tagged with a marker +symbol so that any copy of the CLI can recognise it. `isCommandDefinition(value)` +is the exported check, and it narrows to `DefinedCommand`. The tag survives a +spread, so `{ ...baseDefinition, name: "widget|add2" }` is still recognised. + +`defineCommand` does not register anything by itself — see +[Registering a definition](#registering-a-definition). + +Validation happens where you can see it +--------------------------------------- + +A definition is checked at the moment `defineCommand` is called, not when the +command eventually runs. A misspelled field, a missing `run`, an option +declared with something other than the four helpers, an `arguments` value +outside `"none" | "any"` — each throws immediately, naming the command and the +accepted form: + +``` +Invalid command definition for 'widget|add': unknown field(s) 'handler'; a +definition accepts name, description, options, arguments, canExecute, +disableAnalytics, enableHooks, run. Accepted form: defineCommand({ name: +"widget|add", run(ctx) { ... } }) — with the optional fields description, +options, arguments, canExecute, disableAnalytics and enableHooks. +``` + +Names and the command hierarchy +------------------------------- + +`name` is either a single string or an array of strings, in which case every +entry becomes an alias for the same command. + +The CLI's command registry is flat; the hierarchy the user types on the command +line is encoded in the name with a `|` separator. `"widget|add"` is the command +invoked as `ns widget add`, and `"widget|template|list"` is `ns widget template +list`. Registering a hierarchical name automatically synthesises the parent +dispatcher (`widget`), which routes to the right subcommand or prints help. + +A leading `*` on the last segment marks a **default subcommand**: `"widget|*add"` +runs both for `ns widget add` and for a bare `ns widget`. This is the convention +the CLI's own commands use (`run|*all`, `debug|*all`); the encoding is +user-visible because it feeds shell autocompletion and generated help. + +A parent name cannot also be a command of its own. If `widget` is already +registered as a flat command, registering `widget|add` leaves that command in +place, warns naming both, and creates no dispatcher — so `ns widget add` will +not route until one of the two is renamed. + +Options +------- + +`options` is a schema keyed by the long option name — `output` is passed as +`--output`. Declare each entry with one of the four helpers, which fix the +value type: + +| Helper | Declared with `default` | Declared without | +| --------------- | ----------------------- | ----------------------- | +| `booleanOption` | `boolean` | `boolean \| undefined` | +| `stringOption` | `string` | `string \| undefined` | +| `numberOption` | `number` | `number \| undefined` | +| `arrayOption` | `string[]` | `string[] \| undefined` | + +The two columns are the whole story of the option types: a flag the user did +not pass is absent at runtime, so only a `default` makes the value on +`ctx.options` always present. Declare a default whenever there is a sensible +one and the `| undefined` disappears from the type. + +Each helper takes an optional spec: + +```ts +options: { + // --release, absent means false + release: booleanOption({ default: false }), + // --output , also accepted as -o + output: stringOption({ alias: "o", description: "Output directory" }), + // --retries + retries: numberOption({ default: 3 }), + // --file a.ts --file b.ts + file: arrayOption(), + // kept out of analytics and logs + token: stringOption({ hasSensitiveValue: true }), +} +``` + +- `default` — value used when the flag is absent. +- `alias` — single-dash shorthand, or an array of them (`alias: ["o", "out"]`). +- `hasSensitiveValue` — defaults to `false`; set it for anything that must not + be recorded. There is no reason not to be explicit about credentials, paths + containing user directories, and tokens. +- `description` — reserved for generated help. It reaches the option parser but + nothing renders it yet. + +The schema types `ctx.options` and nothing else: `ctx.options` carries exactly +the declared keys, and a typo is a compile error. Values that the CLI parses +globally (`--path`, `--log`, …) are not exposed there; resolve the `options` +service if you need them. + +### Sharing a schema between commands + +Extract the schema with `satisfies` rather than a type annotation. An +annotation widens every entry back to the general spec type and the `default` +information — and with it the non-optional value types — is lost: + +```ts +const buildOptions = { + release: booleanOption({ default: false }), + output: stringOption({ alias: "o" }), +} satisfies CommandOptionsSchema; +``` + +### Do not shadow a CLI-wide option + +`--verbose`, `--path`, `--log`, `--release`, `--env` and friends are declared by +the CLI itself. Declaring one of those names in a command's schema makes the +command's declaration win for the duration of that command, which means the +same flag means different things depending on which command is running. The CLI +warns at registration naming both sides of the collision; pick another name. + +Aliases count too, in both directions: an `alias: "p"` collides with `--path`'s +shorthand just as `output: stringOption()` would collide with a CLI-wide +`--output`. + +### How validation behaves + +Option validation is the CLI's existing behaviour, not something the definition +opts into. Before a command runs, the parser is re-primed with that command's +declared options and the command line is re-parsed: + +- Declared options are accepted and appear on `ctx.options`. +- An option the CLI does not know — neither global nor declared by this command + — produces a warning: `The option '' is not supported. This will become +an error in a future release.` The command still runs. Set + `NS_STRICT_OPTIONS=error` to preview the hard failure, which is what a future + release will do by default. +- The same staging applies to value-shape violations: a string option passed + with no value, an array option passed nothing, a single-valued option passed + twice. + +So adding an option is a matter of adding a schema entry; forgetting to declare +one that users pass is a warning today and a failure later, never a silent +`undefined`. + +Positional arguments +-------------------- + +`arguments` declares whether the command takes positional arguments at all: + +- `"none"` (the default) — the command accepts no positional arguments. Passing + any is rejected with `This command doesn't accept parameters.` +- `"any"` — positional arguments are accepted and handed to `run` as + `ctx.args`. + +Anything finer than that belongs in `canExecute`. + +### `canExecute` refines, it does not replace + +```ts +defineCommand({ + name: "widget|add", + arguments: "any", + async canExecute(ctx) { + return ctx.args.length === 1; + }, + async run(ctx) { + /* ... */ + }, +}); +``` + +The two fields compose. The declared `arguments` policy is enforced first, and +`canExecute` is consulted only for command lines that already satisfy it — so a +definition that leaves `arguments` at `"none"` still rejects stray positional +arguments even when it supplies a `canExecute`, and a `canExecute` that only +inspects options cannot accidentally widen what the command accepts. + +`canExecute` receives a context of the same shape as `run`'s — the same +`args`, the same declared options and the same `fail` — built freshly for the +call, and returns a boolean (or a promise of one). Returning `false` aborts the +command and prints a bare help suggestion; `ctx.fail(message)` aborts it with +your own message, which is usually the friendlier choice. + +`canExecute` runs inside a dependency-injection context, on the same terms as +`run`: `inject()` is valid up to the first `await`. + +The run context +--------------- + +`run(ctx)` receives: + +- `ctx.args` — `string[]`, the positional arguments left after the command name + (including any subcommand segments) has been consumed. +- `ctx.options` — the current value of each declared option, read at the moment + the command executes. +- `ctx.fail(message)` — fails the command with `message` and a usage help + suggestion. + +`run` may be synchronous or `async`; the CLI awaits the result and treats a +rejection as a command failure. + +### Failing a command + +`ctx.fail(message)` is the idiomatic way to stop a command: + +```ts +defineCommand({ + name: "widget|add", + arguments: "any", + options: { output: stringOption() }, + async run(ctx) { + if (!ctx.options.output) { + ctx.fail("--output is required."); + } + + /* ... */ + }, +}); +``` + +It is available on the `canExecute` context as well, and it returns `never`, so +it can end a branch without a `return`. The message must be a non-empty string. + +Throwing is equivalent and keeps working — `ctx.fail` is sugar over the +`errors` service's `failWithHelp`, which is what adds the "Run `ns widget add +--help`" line. Throw when you already have an `Error` to propagate; call +`ctx.fail` when you are writing the message. + +`run` starts inside a dependency-injection context, so `inject()` works +directly: + +```ts +import { defineCommand, inject } from "nativescript/contracts"; +import { DoctorService } from "nativescript/contracts"; + +export default defineCommand({ + name: "widget|check", + async run() { + const doctorService = inject(DoctorService); + await doctorService.printWarnings(); + }, +}); +``` + +The injection context is synchronous: `inject()` is valid up to the first +`await` in `run`, and not after it. Capture what you need at the top of `run`, +or inject the `Injector` itself and use `injector.get()` for late lookups. See +`dependency-injection.md`. + +Other flags +----------- + +- `disableAnalytics: true` — skips analytics tracking for this command. +- `enableHooks: false` — skips the before/after hooks that normally run around + the command. Hooks are enabled by default. + +Both are simply passed through to the command the CLI executes; omitting them +leaves the CLI's defaults in place. + +Registering a definition +------------------------ + +Inside the CLI, a definition is registered with `registerCommandDefinition`: + +```ts +import { registerCommandDefinition } from "../common/services/command-definition-adapter"; +import addWidgetCommand from "./add-widget"; + +registerCommandDefinition(addWidgetCommand); +``` + +It takes a `DefinedCommand` — the result of `defineCommand`, marker and all — +and rejects a bare object of the right shape, so a definition can never reach +the registry without having been validated. It registers under every name the +definition declares, through the `CommandRegistry` the target injector provides; +pass a second argument to target a different injector (tests do this). The +command instance is built by a factory on first resolution and cached. + +`registerCommandDefinition` lives in +`lib/common/services/command-definition-adapter` rather than in +`nativescript/contracts`, because it reaches into the CLI runtime — the +side-effect-free contracts entry point deliberately does not pull it in. +`defineCommand`, the option helpers and all the types are exported from both +`nativescript/contracts` and `lib/common/define-command`. + +Extensions do not need `registerCommandDefinition` at all: a +`nativescript.commands` manifest entry may point straight at a module that +exports a definition, and the CLI adapts and registers it lazily under the +manifest key (see [extensions.md](extensions.md)). + +Relationship to `ICommand` +-------------------------- + +A definition is compiled into an ordinary `ICommand`, so nothing downstream — +the registry, the router, hooks, help, analytics — knows the difference. The +mapping is: + +| Definition | `ICommand` | +| --------------------------------- | -------------------------------------------------- | +| `options` | `dashedOptions` | +| `run` | `execute`, wrapped in an injection context | +| `arguments`, `canExecute` | `canExecute`: policy enforced, then the refinement | +| — | `allowedParameters`, always `[]` | +| `disableAnalytics`, `enableHooks` | passed through unchanged | + +The compiled command always exposes `canExecute`, because `CommandsService` +stops consulting `allowedParameters` as soon as a command has one — the adapter +therefore enforces the `arguments` policy itself. + +Existing command classes need no migration. Reach for a definition when a +command is mostly "parse these flags and do this"; a class still makes sense +when a command needs constructor-injected collaborators shared across several +methods, custom `ICommandParameter` validators, or a `postCommandAction`. diff --git a/dependency-injection.md b/dependency-injection.md new file mode 100644 index 0000000000..56164068cd --- /dev/null +++ b/dependency-injection.md @@ -0,0 +1,357 @@ +Dependency Injection +==================== + +The NativeScript CLI is migrating from name-based dependency injection (the +`$injector` global, where a constructor parameter named `$doctorService` +resolves the service registered under the string `"doctorService"`) to a typed, +token-based container. Both APIs are backed by **one container**, so they can be +mixed freely: a service registered under a legacy string name is resolvable +through its typed token and vice versa. The legacy `$injector` surface remains +fully supported, is marked `@deprecated` in-editor, and its usage is traced at +runtime so removal can be staged over releases. + +Inside the CLI, import from `lib/common/di`. Extension and hook authors import +the same API from the `nativescript/contracts` subpath (see +[For extension and hook authors](#for-extension-and-hook-authors)). + +At a glance +----------- + +```ts +import { inject, DoctorService } from "nativescript/contracts"; + +class PlatformChecker { + private doctorService = inject(DoctorService); // typed, no decorators needed + + async check(projectDir: string): Promise { + return this.doctorService.canExecuteLocalBuild({ projectDir }); + } +} +``` + +Services that have no typed token yet remain reachable by their registry name — +`inject("logger")` — but that is the migration bridge, not the API: prefer the +token wherever one exists, and mint a token rather than a new string name. + +Tokens: `@Contract` +------------------- + +A token is an abstract class annotated with `@Contract`. The class is both the +compile-time type and the runtime lookup key; the decorator records the token's +canonical string name: + +```ts +import { Contract } from "nativescript/contracts"; + +@Contract({ name: "doctorService" }) // canonical form, no `$` +export abstract class DoctorService { + abstract canExecuteLocalBuild(configuration?: { + platform?: string; + projectDir?: string; + }): Promise; +} +``` + +Rules: + +- **The name is an explicit string literal** — never derived from `class.name`, + which changes under minification. +- Names are minted at this single choke point: declaring two contracts with the + same name **throws at load time**, because a duplicate would silently alias + two tokens. +- The options object leaves room for future fields without changing call sites. +- Implementations do not become tokens by extending or implementing a contract; + only the decorated class itself is a token. + +Tokens for non-classes: `InjectionToken` +---------------------------------------- + +Some registrations have no class to decorate — an imported module namespace, a +plain value, a function. `InjectionToken` is the typed key for those: + +```ts +import { InjectionToken, inject } from "nativescript/contracts"; + +export const XCODE = new InjectionToken( + "xcode", +); + +class ProjectPatcher { + private xcode = inject(XCODE); // typeof import("nativescript-dev-xcode") +} +``` + +The description **is** the registry name, exactly as a contract's name is, so a +token is a typed alias onto an existing registration and nothing has to change +where the value is registered — `injector.register("xcode", xcode)` keeps +serving `inject(XCODE)`. A leading `$` in the description is stripped. + +Names are minted in the same registry `@Contract` uses, so a token and a +contract cannot claim the same name: the second one **throws at load time**, +rather than silently aliasing one registration under two tokens. + +Tokens are used anywhere a contract class is — `inject()`, `get()`, `provide()` +and the provider literals: + +```ts +{ provide: XCODE, useValue: xcode } +{ provide: XCODE, useFactory: () => require("nativescript-dev-xcode") } +``` + +Prefer a `@Contract` class when the dependency is a service: it also carries +the service's shape. Reach for `InjectionToken` only when there is nothing to +decorate. + +Resolving: `inject()` and `Injector` +------------------------------------ + +`inject(token)` returns the singleton for a token from the current injection +context. It is **synchronous by design** and valid only: + +- in field initializers, +- in constructor bodies, +- in provider factories, +- inside an explicit `runInInjectionContext(injector, fn)`. + +It is **not** valid after an `await`. For late or conditional lookups, +self-inject the `Injector` and use `get()`: + +```ts +import { inject, Injector } from "nativescript/contracts"; + +class EnvironmentChecker { + private injector = inject(Injector); + + async check(projectDir: string) { + await somethingAsync(); + return this.injector.get(DoctorService); // fine after await + } +} +``` + +`Injector.get()` accepts a contract class, an `InjectionToken`, a string name, +or a `$`-prefixed string name — all of them return the same instance. The +string forms exist for interoperability with the legacy registry; use the token +whenever one exists. + +Both `inject()` and `get()` take Angular-shaped options as their second +argument: + +```ts +inject(DoctorService, { optional: true }); // DoctorService | null — no throw +inject("logger", { skipSelf: true }); // start at the parent: escapes a + // child scope's shadowing entry +inject("options", { self: true }); // this level only — no fallthrough +``` + +`optional` covers not-found only; a found-but-misconfigured provider still +throws. `self` and `skipSelf` cannot be combined. There is deliberately no +`host` option: it is an Angular component-tree concept with no analog in the +CLI's injector hierarchy. + +The injection context is shared process-wide. If a hook or extension module +ends up resolving a *duplicated* copy of the CLI (a nested `nativescript` +install, or a project-local copy under a globally-run CLI), its `inject()` +still resolves against the running CLI's context — with a one-time warning, +because a duplicated copy loads the CLI twice. Declaring `nativescript` as a +`peerDependency` lets the running copy be shared instead. + +Registering: providers +---------------------- + +```ts +import { provide, provideLazy, Injector } from "nativescript/contracts"; + +const injector = new Injector([ + // eager class binding; type-checked: the impl must satisfy the token + provide(DoctorService, DoctorServiceImpl), + + // deferred loading: the module is require()d on first resolution only + provideLazy(DoctorService, () => require("./doctor-service").DoctorServiceImpl), + + { provide: Config, useValue: { DISABLE_HOOKS: false } }, + { provide: Dispatcher, useFactory: () => createDispatcher(), shared: false }, +]); + +// registration is also allowed after construction; re-registering a token +// updates the existing record in place +injector.register(provide(ProjectNameService, ProjectNameServiceImpl)); +``` + +Provider kinds: + +| Kind | Shape | Notes | +|---|---|---| +| Class | `provide(Token, Impl)` / `{ provide, useClass }` | constructed with `new Impl()` inside an injection context, so `inject()` works in its fields | +| Lazy class | `provideLazy(Token, () => Impl)` / `{ provide, useLazyClass }` | loader runs on first `get()` only — keeps startup lazy | +| Value | `{ provide, useValue }` | registered instance; re-registering replaces the cached instance. With `shared: false` there is no resolver and `get()` throws — a preserved legacy quirk | +| Factory | `{ provide, useFactory }` | called inside an injection context | + +`shared: false` makes a provider transient: every resolution constructs a fresh +instance. Transient instances are still retained by the container so +`dispose()` reaches them. + +String keys are accepted anywhere a token is (`{ provide: "logger", useValue }`) +— that is how the legacy facade registers, and how per-call overrides address +not-yet-migrated dependencies. New registrations should mint a `@Contract` +class, or an `InjectionToken` when there is no class to decorate, instead of a +new string name. + +For per-call construction with overrides (a fresh instance of a class with some +dependencies replaced), use `createInstance`: + +```ts +const debugService = injector.createInstance(IOSDeviceDebugService, [ + { provide: "device", useValue: device }, +]); +``` + +Overrides shadow **one level deep only** — the direct dependencies of the class +being constructed. Nested dependencies are constructed by the injector that +owns them and never see the per-call providers. + +Resolution semantics +-------------------- + +- Lookup is **token identity first, token name on a miss**, checked per + injector level before delegating to the parent. Both keys index the same + provider record, so re-registering a service by its string name (as plugins + are documented to do with `$logger`) stays visible to `inject(Logger)` + consumers. This holds for `@Contract` classes and `InjectionToken`s alike. +- A leading `$` is stripped from string tokens: `get("$fs")` and `get("fs")` + are the same registration. +- The name fallback also makes **duplicated token copies interchangeable**: if + an extension's dependency tree carries its own copy of a contract class or + injection token, that copy resolves to the same provider by name. "Works + locally, breaks when installed" is not a failure mode of this design. +- Cyclic dependencies fail with the full resolution path + (`Cyclic dependency detected on dependency 'a'. Resolution path: a -> b -> a`). + +Child scopes +------------ + +`injector.createChild(providers)` creates a scope that shadows its parent for +the given tokens and falls through for everything else. Sibling scopes are +isolated. Scopes are how per-invocation data (hook payloads, per-call +overrides) is layered over the shared singletons without ever entering the +root container. + +`forwardRef` +------------ + +Provider arrays are evaluated at module load. When a token is declared later in +the same file (TDZ) or reached through a circular import, wrap the reference in +a thunk; it is read only when the injector processes the provider: + +```ts +import { forwardRef } from "nativescript/contracts"; + +const providers = [ + { provide: forwardRef(() => DoctorService), useClass: DoctorServiceImpl }, +]; +``` + +`forwardRef` defers *references*, not construction — it cannot break an +instantiation cycle between two services. For that, self-inject the `Injector` +and resolve late (see above). + +Working alongside the legacy `$injector` +---------------------------------------- + +The `Yok` facade (`global.$injector`) IS an `Injector` — the class extends the +token-based container — so the new API works on it directly: + +```ts +$injector.resolve("doctorService") === $injector.get(DoctorService); // true +$injector.register(provide(DoctorService, DoctorServiceImpl)); +runInInjectionContext($injector, () => inject(DoctorService)); +``` + +- Legacy string names are permanent: a contract's token name is its interop + identity, used by hooks, plugins, and the public API. Nothing is deleted + per-service. +- Every legacy member (`resolve`, `register`, `require*`, the command-registry + surface) carries `@deprecated` JSDoc naming its replacement. +- Legacy usage at the external entry points (param-name hooks, require-time + extension registration, help templating) is reported through a deprecation + tracer. It logs at trace level today; set `NS_DEPRECATIONS=warn` or + `NS_DEPRECATIONS=error` to preview the stricter stages that later releases + will default to. + +For extension and hook authors +------------------------------ + +Depend on `nativescript` itself (as a `peerDependency`, plus a `devDependency` +for local development) and import from the `contracts` subpath: + +```ts +import { inject, DoctorService } from "nativescript/contracts"; +``` + +- The subpath resolves through a directory `package.json` — the CLI's + `package.json` deliberately has **no `exports` map**, so any deep `require()` + paths you already use keep working. +- The entry point is side-effect-free: importing it never boots a CLI runtime, + even from a duplicated copy in your dependency tree. +- The existing `$injector`-based extension and hook mechanisms keep working + unchanged; the typed API is additive. + +Available contracts +------------------- + +Growing as services migrate. Every token below is a typed alias onto the +registration it names — resolving by token and resolving by the legacy name +return the same instance. + +The contract is also the single source of truth for the service's shape: the +ambient interface the CLI has always published extends it (`interface ILogger +extends Logger {}`), so the two cannot drift apart. Add a member to the +contract and every existing caller sees it. + +| Token | Legacy name | +|---|---| +| `ChildProcess` | `childProcess` | +| `DevicesService` | `devicesService` | +| `DoctorService` | `doctorService` | +| `Errors` | `errors` | +| `FileSystem` | `fs` | +| `HostInfo` | `hostInfo` | +| `HttpClient` | `httpClient` | +| `Logger` | `logger` | +| `PackageManager` | `packageManager` | +| `ProjectData` | `projectData` | +| `ProjectDataService` | `projectDataService` | +| `ProjectNameService` | `projectNameService` | +| `Prompter` | `prompter` | +| `TempService` | `tempService` | +| `ViteHmrPortService` | `viteHmrPortService` | + +And the injection tokens, for registrations that are not classes: + +| Token | Legacy name | Value | +|---|---|---| +| `XCODE` | `xcode` | the `nativescript-dev-xcode` module | +| `PBXPROJ_DOM_XCODE` | `pbxprojDomXcode` | the `pbxproj-dom/xcode` module | + +Related guides +-------------- + +- [defining-commands.md](defining-commands.md) — declarative, typed commands via `defineCommand`. +- [extensions.md](extensions.md) — extension authoring, including the `nativescript.commands` manifest. +- [extending-cli.md](extending-cli.md) — hooks, including the typed `defineHook` API. + +Legacy → new quick reference +---------------------------- + +`di` below is any `Injector` you hold — including `$injector` itself, which +extends `Injector`. + +| Legacy (`$injector`) | New | +|---|---| +| `resolve("name")` | `inject(Token)` in an injection context, or `di.get(Token)` | +| `resolve(SomeClass)` / `resolve(SomeClass, { dep })` | `di.createInstance(SomeClass, [{ provide: "dep", useValue }])` | +| `register("name", Impl)` | `di.register(provide(Token, Impl))` | +| `register("name", instance)` | `di.register({ provide: Token, useValue: instance })` | +| `register("name", Impl, false)` | `di.register({ provide: Token, useClass: Impl, shared: false })` | +| `require("name", "./path")` | `provideLazy(Token, () => require("./path").Impl)` | +| constructor param `$name` | `inject(Token)` field initializer | diff --git a/dev/tsc-to-mocha-watch.js b/dev/tsc-to-mocha-watch.js deleted file mode 100644 index 6fa25998fd..0000000000 --- a/dev/tsc-to-mocha-watch.js +++ /dev/null @@ -1,57 +0,0 @@ -// Run "tsc" with watch, upon successful compilation run mocha tests. - -var child_process = require("child_process"); -var spawn = child_process.spawn; -var readline = require("readline"); -var chalk = require("chalk"); - -var mocha = null; -var mochal = null; -var errors = 0; - -function compilationStarted() { - if (mocha) { - mocha.kill('SIGINT'); - } - mocha = null; - mochal = null; - errors = 0; -} -function foundErrors() { - errors ++; -} -function compilationComplete() { - if (errors) { - console.log(" " + chalk.red("TS errors. Will not start mocha.")); - return; - } else { - console.log(" " + chalk.gray("Run mocha.")); - } - mocha = spawn("./node_modules/.bin/mocha", ["--colors"]); - mocha.on('close', code => { - if (code) { - console.log(chalk.gray("mocha: ") + "Exited with " + code); - } else { - console.log(chalk.gray("mocha: ") + chalk.red("Exited with " + code)); - } - mocha = null; - mochal = null; - }); - mochal = readline.createInterface({ input: mocha.stdout }); - mochal.on('line', line => { - console.log(chalk.gray('mocha: ') + line); - }); -} - -var tsc = spawn("./node_modules/.bin/tsc", ["--watch"]); -var tscl = readline.createInterface({ input: tsc.stdout }); -tscl.on('line', line => { - console.log(chalk.gray(" tsc: ") + line); - if (line.indexOf("Compilation complete.") >= 0) { - compilationComplete(); - } else if (line.indexOf("File change detected.") >= 0) { - compilationStarted(); - } else if (line.indexOf(": error TS") >= 0) { - foundErrors(); - } -}); diff --git a/dev/tsc-to-vitest-watch.js b/dev/tsc-to-vitest-watch.js new file mode 100644 index 0000000000..b2371d4f5a --- /dev/null +++ b/dev/tsc-to-vitest-watch.js @@ -0,0 +1,37 @@ +// Run "tsc" in watch mode alongside vitest. Vitest runs the compiled output, +// so on its own it never notices a .ts edit - tsc has to re-emit first, and +// vitest picks the change up from there. + +const { spawn } = require("child_process"); + +// shell: true so the node_modules/.bin shims resolve on Windows as well +const spawnOptions = { stdio: "inherit", shell: true }; + +const children = [ + // --preserveWatchOutput keeps tsc from clearing the screen and wiping the + // test results out from under you on every recompile + spawn("tsc", ["--watch", "--preserveWatchOutput"], spawnOptions), + // --watch explicitly: vitest only infers watch mode when stdout is a TTY, + // and would otherwise run once and exit, taking tsc down with it + spawn("vitest", ["--watch"], spawnOptions), +]; + +let shuttingDown = false; + +function shutdown() { + if (shuttingDown) { + return; + } + shuttingDown = true; + for (const child of children) { + child.kill("SIGINT"); + } +} + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); + +for (const child of children) { + // if either side dies, don't leave the other running in the background + child.on("exit", shutdown); +} diff --git a/docs/build-jekyll-md.sh b/docs/build-jekyll-md.sh index 766f974aa7..fdff872477 100755 --- a/docs/build-jekyll-md.sh +++ b/docs/build-jekyll-md.sh @@ -4,4 +4,4 @@ set -e rm -rf docs-cli npm install --ignore-scripts -npx grunt docs-jekyll +npm run docs-jekyll diff --git a/docs/man_pages/config/config-set.md b/docs/man_pages/config/config-set.md index ea7769f647..78e9652221 100644 --- a/docs/man_pages/config/config-set.md +++ b/docs/man_pages/config/config-set.md @@ -31,6 +31,8 @@ General | `$ ns config set ` * Setting whole objects is not supported. Update individual keys instead. For example, use: `$ ns config set android.codeCache true` +<% if(isHtml) { %> + ### Related Commands Command | Description diff --git a/docs/man_pages/project/testing/build-android.md b/docs/man_pages/project/testing/build-android.md index 2904ff952e..d537fa081c 100644 --- a/docs/man_pages/project/testing/build-android.md +++ b/docs/man_pages/project/testing/build-android.md @@ -13,7 +13,7 @@ Builds the project for Android and produces an APK that you can manually deploy Usage | Synopsis ---|--- -General | `$ ns build android [--compileSdk ] [--key-store-path --key-store-password --key-store-alias --key-store-alias-password ] [--release] [--static-bindings] [--copy-to ] [--env.*]] [--aab]` +General | `$ ns build android [--compileSdk ] [--key-store-path --key-store-password --key-store-alias --key-store-alias-password ] [--release] [--copy-to ] [--env.*]] [--aab]` ### Options @@ -34,6 +34,7 @@ General | `$ ns build android [--compileSdk ] [--key-store-path ` - Specifies the directory that contains the project. If not set, the project is searched for in the current directory and all directories above it. diff --git a/docs/man_pages/project/testing/debug-android.md b/docs/man_pages/project/testing/debug-android.md index cd34d57c66..44116cc3be 100644 --- a/docs/man_pages/project/testing/debug-android.md +++ b/docs/man_pages/project/testing/debug-android.md @@ -38,6 +38,7 @@ Attach the debug tools to a running app in the native emulator | `$ ns debug and * `--env.sourceMap` - creates inline source maps. * `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release). * `--aab` - Specifies that the command will produce and deploy an Android App Bundle. +* `--gradleFlavor` - Builds the given product flavor, when the app declares any. `--gradleFlavor foo` runs the `assembleFooDebug`/`assembleFooRelease` gradle task instead of `assembleDebug`/`assembleRelease`. * `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. Otherwise, the command will check the application compatibility with the current CLI version and could fail requiring `ns migrate`. <% if(isHtml) { %> diff --git a/docs/man_pages/project/testing/run-android.md b/docs/man_pages/project/testing/run-android.md index c895cd8103..ab651eff42 100644 --- a/docs/man_pages/project/testing/run-android.md +++ b/docs/man_pages/project/testing/run-android.md @@ -43,6 +43,7 @@ Start a default emulator if none are running, or run application on all connecte * `--env.sourceMap` - creates inline source maps. * `--env.hiddenSourceMap` - creates sources maps in the root folder (useful for Crashlytics usage with bundled app in release). * `--aab` - Specifies that the command will produce and deploy an Android App Bundle. +* `--gradleFlavor` - Builds the given product flavor, when the app declares any. `--gradleFlavor foo` runs the `assembleFooDebug`/`assembleFooRelease` gradle task instead of `assembleDebug`/`assembleRelease`. * `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. Otherwise, the command will check the application compatibility with the current CLI version and could fail requiring `ns migrate`. <% if(isHtml) { %> diff --git a/docs/man_pages/project/testing/test-android.md b/docs/man_pages/project/testing/test-android.md index efa789469a..46d3f460a5 100644 --- a/docs/man_pages/project/testing/test-android.md +++ b/docs/man_pages/project/testing/test-android.md @@ -43,4 +43,5 @@ Command | Description --------|------------ [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. +[test visionos](test-visionos.html) | Runs the tests in your project in the visionOS Simulator or on Apple Vision Pro devices. <% } %> diff --git a/docs/man_pages/project/testing/test-init.md b/docs/man_pages/project/testing/test-init.md index c19827d161..638295d649 100644 --- a/docs/man_pages/project/testing/test-init.md +++ b/docs/man_pages/project/testing/test-init.md @@ -9,6 +9,8 @@ position: 21 Configures your project for unit testing with a selected framework. This operation installs the @nativescript/unit-test-runner npm module and its dependencies and creates a `tests` folder in the `app` directory. +The recommended framework is `vitest`, which runs your specs inside real NativeScript runtimes on device/emulator and also supports UI testing. The Karma-based frameworks (jasmine, mocha, qunit) are deprecated and will be removed in a future release. + ### Commands Usage | Synopsis @@ -17,7 +19,7 @@ General | `$ ns test init [--framework ]` ### Options -* `--framework ` - Sets the unit testing framework to install. The following frameworks are available: mocha, jasmine and qunit. +* `--framework ` - Sets the unit testing framework to install. The following frameworks are available: vitest (recommended), mocha, jasmine and qunit (deprecated). <% if(isHtml) { %> @@ -31,4 +33,5 @@ Command | Description --------|------------ [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. [test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. +[test visionos](test-visionos.html) | Runs the tests in your project in the visionOS Simulator or on Apple Vision Pro devices. <% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/test-ios.md b/docs/man_pages/project/testing/test-ios.md index 8a421559a8..dac2ccfccb 100644 --- a/docs/man_pages/project/testing/test-ios.md +++ b/docs/man_pages/project/testing/test-ios.md @@ -45,4 +45,5 @@ Command | Description --------|------------ [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. +[test visionos](test-visionos.html) | Runs the tests in your project in the visionOS Simulator or on Apple Vision Pro devices. <% } %> diff --git a/docs/man_pages/project/testing/test-visionos.md b/docs/man_pages/project/testing/test-visionos.md new file mode 100644 index 0000000000..af48d8175c --- /dev/null +++ b/docs/man_pages/project/testing/test-visionos.md @@ -0,0 +1,46 @@ +<% if (isJekyll) { %>--- +title: ns test visionos +position: 24 +---<% } %> + +# ns test visionos + +### Description + +Runs the tests in your project in the visionOS Simulator or on connected Apple Vision Pro devices.<% if(isConsole && isMacOS) { %> Your project must already be configured for unit testing with the Vitest framework by running `$ ns test init --framework vitest`.<% } %> Unit testing on visionOS requires the Vitest testing framework; the deprecated Karma-based frameworks are not supported on this platform. + +<% if(isConsole && (isLinux || isWindows)) { %>WARNING: You can run this command only on macOS systems. To view the complete help for this command, run `$ ns help test visionos`<% } %> + +### Commands + +Usage | Synopsis +------|------- +Run tests in the visionOS Simulator | `$ ns test visionos` +Run tests on a selected device | `$ ns test visionos --device ` + +<% if((isConsole && isMacOS) || isHtml) { %> + +### Options + +* `--device` - Specifies the serial number or the index of the connected device on which you want to run tests. To list all connected devices, grouped by platform, run `$ ns device`. `` is the device index or identifier as listed by the `$ ns device` command. +* `--env.codeCoverage` - If set, collects code coverage for the test run. +* `--force` - If set, skips the application compatibility checks and forces `npm i` to ensure all dependencies are installed. Otherwise, the command will check the application compatibility with the current CLI version and could fail requiring `ns migrate`. + +<% } %> + +<% if(isHtml) { %> + +### Prerequisites + +* Verify that [you have configured your project for unit testing](test-init.html) with the Vitest framework. +* Verify that [you have stored your unit tests in `app` → `tests`](http://docs.nativescript.org/testing). +* Verify that [you have configured your system and devices properly](http://docs.nativescript.org/testing). + +### Related Commands + +Command | Description +--------|------------ +[test init](test-init.html) | Configures your project for unit testing with a selected framework. +[test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. +[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. +<% } %> diff --git a/docs/man_pages/project/testing/test.md b/docs/man_pages/project/testing/test.md index 5da36cbe25..3a77080cae 100644 --- a/docs/man_pages/project/testing/test.md +++ b/docs/man_pages/project/testing/test.md @@ -28,7 +28,8 @@ Usage | Synopsis <% if((isConsole && isMacOS) || isHtml) { %>### Arguments `` is the target mobile platform on which you want to run the tests. You can set the following target platforms. * `android` - Runs the tests in your project on connected Android devices or Android emulators. -* `ios` - Runs the tests in your project on connected iOS devices.<% } %> +* `ios` - Runs the tests in your project on connected iOS devices. +* `visionos` - Runs the tests in your project in the visionOS Simulator or on connected Apple Vision Pro devices. Requires the Vitest testing framework.<% } %> <% if(isHtml) { %> @@ -45,4 +46,5 @@ Command | Description [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. [test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. +[test visionos](test-visionos.html) | Runs the tests in your project in the visionOS Simulator or on Apple Vision Pro devices. <% } %> diff --git a/extending-cli.md b/extending-cli.md index 3a7b5387b5..8808a4fde1 100644 --- a/extending-cli.md +++ b/extending-cli.md @@ -11,7 +11,7 @@ For the NativeScript CLI to execute your hooks, you must place them in the `hook You can attach the hook before or after `prepare` operations or to `--watch` operations. -Note that `watch` hooks can be executed only at the time of running `--watch` operations. The `watch` hooks are the last thing executed before launching the file system watcher which tracks for changes to your code. +Note that `watch` hooks can be executed only at the time of running `--watch` operations. The `before-watch` hooks are the last thing executed before launching the file system watcher which tracks for changes to your code. Your hooks must conform to the following naming and placement conventions: @@ -36,27 +36,29 @@ Your hooks must conform to the following naming and placement conventions: ├── hook1 (this is an executable file) └── hook2 (this is an executable file) ``` -* If you want to attach a hook for `--watch` operations, you must place the hook in the root of the `hooks` subdirectory. The file must be named `watch`. For example: +* If you want to attach a hook for `--watch` operations, you must place the hook in the root of the `hooks` subdirectory. The file must be named `before-watch` or `after-watch`. For example: ``` my-app/ ├── index.js ├── package.json └── hooks/ - └── watch.js (this is a Node.js script) + └── before-watch.js (this is a Node.js script) ``` -* If you want to attach multiple hooks for `--watch` operations, you must place them inside a `watch` subdirectory of the `hooks` subdirectory. You can specify any meaningful name for the the hooks inside the subdirectory. For example: +* If you want to attach multiple hooks for `--watch` operations, you must place them inside a `before-watch` or `after-watch` subdirectory of the `hooks` subdirectory. You can specify any meaningful name for the the hooks inside the subdirectory. For example: ``` my-app/ ├── index.js ├── package.json └── hooks/ - └── watch (a directory) + └── before-watch (a directory) ├── hook1 (this is an executable file) └── hook2 (this is an executable file) ``` + A file named plainly `watch` is never executed: like every other hook point, the watch hooks are addressed by the `before-`/`after-` names above. + > **NOTE:** When multiple hooks are attached to a single event (i.e. multiple hooks are stored in dedicated subdirectories), at the specified time, the CLI executes each hook one by one. However, the order of hook execution is not strict and might change over command executions. Execute Hooks as Child Process @@ -77,7 +79,134 @@ Execute Hooks In-Process When your hook is a Node.js script, the CLI executes it in-process. This gives you access to the entire internal state of the CLI and all of its functions. -The CLI assumes that this is a CommonJS module and calls its single exported function with four parameters. The type of the parameters is described in the `.d.ts` files which are part of the CLI source code [here](https://github.com/NativeScript/nativescript-cli/tree/master/lib/definitions) and [here](https://github.com/telerik/mobile-cli-lib/tree/master/definitions). +The CLI assumes that this is a CommonJS module and calls the hook it exports — either a hook definition (see below) or a plain function. + +## Writing a hook + +Export a hook definition built with `defineHook`. It takes the hook point in the usual naming convention (`before-prepare`, `after-watch`) and a `run` handler that receives a context object. + +```JavaScript +const { defineHook, inject, DoctorService } = require("nativescript/contracts"); + +module.exports = defineHook({ + name: "before-prepare", + run: async (ctx) => { + const doctorService = inject(DoctorService); + await doctorService.canExecuteLocalBuild(); + }, +}); +``` + +`defineHook(name, run)` is shorthand for the same definition: + +```JavaScript +module.exports = defineHook("before-prepare", async (ctx) => { /* ... */ }); +``` + +`defineHook` validates its input immediately: a missing or non-string `name`, a missing or non-function `run`, and unknown fields all throw at definition time, naming the definition and both accepted forms. + +The `name` decides when the hook fires and must match the hook point the file is placed at. A definition whose `name` disagrees with its location is **skipped with a warning** rather than run at the wrong point. Export exactly one definition (or one plain function) per file — an array export is rejected. + +Services come from `inject()` — the same API used everywhere else (see [dependency-injection.md](dependency-injection.md)): + +* `inject()` is valid in the synchronous part of the handler — not after an `await`. Resolve what you need up front; for late lookups, grab the container first: `const injector = inject(Injector)` (`Injector` is exported from `nativescript/contracts` too), then `injector.get(...)` later. +* Tokens resolve by class first and by their canonical name on a miss, so this works even if your dependency tree carries its own copy of `nativescript` — a duplicated token class still resolves to the running CLI's service. +* Only a first tranche of services has typed tokens so far ([dependency-injection.md](dependency-injection.md#available-contracts) lists them); a service without a token is reachable by its registry name — `inject("logger")` — as a migration bridge. +* If you build your hook in TypeScript, add `nativescript` as a `devDependency` and import the same names: `import { defineHook, inject, DoctorService } from "nativescript/contracts"`. An `.mjs` hook can `export default defineHook(...)`. + +### `ctx.payload` + +`ctx.payload` holds the parameters of the CLI operation being hooked; its shape depends on the hook point. It is the CLI's own object, so mutating it influences the operation: + +```JavaScript +module.exports = defineHook("before-build-task-args", (ctx) => { + ctx.payload.args.push("--offline"); +}); +``` + +Not every invocation carries one. The `before-`/`after-` hooks fired around command dispatch (`before-build`, `after-run`, …) pass no arguments at all, so `ctx.payload` is `undefined` there. Treat it as optional — in TypeScript it is typed `TPayload | undefined`: + +```TypeScript +import { defineHook } from "nativescript/contracts"; + +export default defineHook<{ args: string[] }>("before-build-task-args", (ctx) => { + ctx.payload?.args.push("--offline"); +}); +``` + +### `ctx.wrap(middleware)` + +`ctx.wrap(middleware)` puts a middleware around the hooked method. The middleware receives the method's arguments and a `next` callback; call `next` to continue, or return without calling it to short-circuit the method entirely. + +```JavaScript +module.exports = defineHook("before-prepare", (ctx) => { + ctx.wrap(async (args, next) => { + const result = await next(...args); + return result; + }); +}); +``` + +Only a hook point that actually folds middlewares around a method can honor `wrap()`, so it is available **only in the before-phase of the wrappable hook points** listed below. Calling it anywhere else — from any `after-` hook, or from a before-hook at a non-wrappable point — throws an error naming the hook point instead of registering a middleware that would never run. + +The wrappable hook points are: + +`before-buildAndroid` · `before-buildAndroidPlugin` · `before-buildIOS` · `before-checkEnvironment` · `before-checkForChanges` · `before-install` · `before-prepare` · `before-prepareNativeApp` · `before-resolveCommand` · `before-watch` · `before-watchPatterns` + +### `ctx.fail(message)` and `ctx.skip(message)` + +Both end the handler immediately — nothing after the call runs — and differ in what happens to the command. + +`ctx.fail(message)` fails the command, printing `message` as the error: + +```JavaScript +module.exports = defineHook("before-prepare", (ctx) => { + ctx.fail("The generated bundle is missing; run the bundler first."); +}); +``` + +`ctx.skip(message)` prints `message` as a warning and lets the command continue: + +```JavaScript +module.exports = defineHook("before-prepare", (ctx) => { + ctx.skip("Nothing to prepare."); +}); +``` + +The message is required in practice — calling either without one falls back to a message naming the hook point and the method. + +### Plain function hooks + +Exporting a plain function is still supported. It runs in an injection context too, so `inject()` works the same way; declare a `hookArgs` parameter if you need the payload. + +```JavaScript +const { inject, DoctorService } = require("nativescript/contracts"); + +module.exports = function (hookArgs) { + const doctorService = inject(DoctorService); + return doctorService.canExecuteLocalBuild(); +}; +``` + +## The hook contract + +The hook must return a Promise. If the hook succeeds, it must fullfil the promise, but the fullfilment value is ignored. +The hook can also reject the promise with an instance of Error. The returned error can carry two members that together downgrade the rejection to a warning. + +Member | Type | Description +---|---|--- +`errorAsWarning` | Boolean | Must be exactly `true`. The CLI prints the error.message colored as a warning and continues executing the current command. +`stopExecution` | Boolean | Must be present and of type Boolean. It only enables the check — setting it alone, with either value, changes nothing. + +**Both** members are required: the CLI continues only when `errorAsWarning === true` *and* `stopExecution` is a Boolean. Otherwise it prints the returned error colored as a fatal error and stops executing the current command. + +A plain-function hook can also return a function, which the CLI folds into a middleware chain around the hooked method. + +With `defineHook` neither convention is needed, and neither applies: `ctx.fail`/`ctx.skip` replace throwing an error carrying `stopExecution`/`errorAsWarning`, and `ctx.wrap` replaces returning a function. A definition whose `run` returns a function is warned about — the returned function is not used as a middleware. + +## Legacy: parameter-name injection + +Historically, a hook received CLI services by naming them as parameters: the CLI parses the exported function's parameter names and injects the service registered under each name. Existing hooks written this way keep working unchanged, but **new hooks should use the pattern above** — parameter-name service injection is slated for deprecation, and hooks that use it are reported through the CLI's deprecation tracer (visible with `--log trace`, or as warnings with `NS_DEPRECATIONS=warn`). Parameter | Type | Description ---|---|--- @@ -85,18 +214,8 @@ Parameter | Type | Description `$projectData` | IProjectData | Contains data about the project, such as project directory, ID, dependencies, etc. `$usbLiveSyncService` | ILiveSyncService | Use this variable to check whether a LiveSync or normal build is in progress. `hookArgs` | Any | Contains all the parameters of the original function in the CLI which is being hooked. - -The hook must return a Promise. If the hook succeeds, it must fullfil the promise, but the fullfilment value is ignored. -The hook can also reject the promise with an instance of Error. The returned error can have two optional members controlling the CLI. - -Member | Type | Description ----|---|--- -`stopExecution` | Boolean | Set this to `false` to let the CLI continue executing this command. -`errorAsWarning` | Boolean | Set this to treat the returned error as warning. The CLI prints the error.message colored as a warning and continues executing the current command. - -If these two members are not set, the CLI prints the returned error colored as fatal error and stops executing the current command. - -Furthermore, the global variable `$injector` of type `IInjector` provides access to the CLI Dependency Injector, through which all code services are available. + +The type of the parameters is described in the `.d.ts` files which are part of the CLI source code [here](https://github.com/NativeScript/nativescript-cli/tree/master/lib/definitions). Any registered service name is injectable, not only the ones listed; the global variable `$injector` of type `IInjector` likewise remains available. A parameter the CLI cannot resolve causes the hook to be skipped with a warning. Commands with Hooking Support ============================== diff --git a/extensions.md b/extensions.md new file mode 100644 index 0000000000..93f882f1fb --- /dev/null +++ b/extensions.md @@ -0,0 +1,289 @@ +Writing a CLI Extension +======================= + +An extension adds new commands to the NativeScript CLI. Extensions are ordinary +npm packages. + +```bash +ns extension install +ns extension uninstall +``` + +Installed extensions live in the CLI profile directory, under +`extensions/node_modules/`, and every CLI invocation consults each +of them. That makes the manifest below the most important file in an extension: +it is what the CLI reads on startup, and it decides whether your code is loaded +eagerly or only when one of your commands is actually executed. + +Depending on the CLI +-------------------- + +An extension that imports anything from the CLI — `defineCommand`, `inject`, the +types — needs `nativescript` declared twice: + +```json +{ + "name": "nativescript-hello", + "version": "1.0.0", + "keywords": ["nativescript:extension"], + "peerDependencies": { + "nativescript": ">=9.1.0" + }, + "devDependencies": { + "nativescript": "^9.1.0" + } +} +``` + +- The **peer dependency** declares which CLI versions the extension works with, + and keeps package managers from installing a second copy of the CLI next to + your extension. Your code must run against the _running_ CLI: a second copy + brings its own injector, and services resolved from it are not the ones + executing the command. +- The **dev dependency** is what makes `require("nativescript/contracts")` + resolve while you build and test the extension. It is not installed for your + users. +- Developing against a **prerelease** CLI? Semver ranges without a prerelease + tag never match one — `9.1.0-alpha.15` does not satisfy `>=9.1.0` — so pin + the exact prerelease as your dev dependency and keep the stable floor in + `peerDependencies`. + +Never declare `nativescript` as a plain dependency. + +`nativescript/contracts` is the entry point extensions import from. It is +side-effect free — importing it does not boot a CLI — and it exports +`defineCommand`, `inject`, the option helpers and the public types. + +The `nativescript:extension` keyword makes the package discoverable: the CLI +searches npm for it when it needs to suggest an extension for an unknown command +(see [Suggesting an extension](#suggesting-an-extension-for-an-unknown-command)). + +> Extensions are installed per user today, and are available from every project +> on the machine. Installing them as project `devDependencies` — pinned per +> project, reproducible in CI, shared with the team — is the direction this is +> heading; declaring the peer dependency now is what makes an extension ready +> for it. + +Declaring commands +------------------ + +Commands are declared in the `commands` key of the `nativescript` key of the +extension's `package.json`. Two shapes are accepted. + +### A map of command name to module (recommended) + +```json +{ + "nativescript": { + "commands": { + "hello|world": "./dist/commands/hello-world.js", + "hello|*default": "./dist/commands/hello.js" + } + } +} +``` + +Each key is a command name; each value says where the module implementing it +lives, resolved relative to the extension's root directory. A value is either +the path itself or an object carrying it under `path`: + +```json +{ + "nativescript": { + "commands": { + "hello|world": { "path": "./dist/commands/hello-world.js" } + } + } +} +``` + +The two forms mean exactly the same thing today. Keys the CLI does not +recognise inside the object form are ignored, so the object can carry +information a later CLI understands without breaking the one you have +installed. + +Declaring commands this way is strongly preferred: + +- **Per-command lazy loading.** Nothing in the extension is loaded when the CLI + starts. A command's module is required the first time that command is + resolved, so `ns build android` never pays the cost of loading an unrelated + extension. With a large or dependency-heavy extension installed, that is the + difference between a noticeable startup delay on every command and none. + Dispatching `ns hello world` loads only `hello-world.js` — not the sibling + `hello.js`, and not the extension's main entry. +- **Early, named conflict detection.** Two extensions claiming the same command + name is reported as a warning that names both extensions and the contested + command, and the extension that claimed it first keeps working. Under the + legacy shape the same collision surfaces as an opaque + `module '...' require'd twice.` failure from whichever extension happened to + load second. +- **The CLI knows what you contribute without running you.** The declared + command names are what the install suggestion for an unknown command matches + against, and they are available to the CLI as metadata about the installed + extension. + +Malformed entries are skipped rather than fatal: an entry whose command name is +not a non-empty string, or whose value carries no usable module path, is +reported as a warning naming the extension and the offending entry, and the +extension's remaining commands are still registered. + +An empty map opts out of loading entirely: + +```json +{ + "nativescript": { + "commands": {} + } +} +``` + +The extension contributes no commands, and — unlike omitting the key — its main +entry is never required. Use it for an extension that only ships documentation +or assets. + +### An array of command names (legacy) + +```json +{ + "nativescript": { + "commands": ["hello|world", "hello|*default"] + } +} +``` + +The array is a discovery aid only — it lists the names the CLI may suggest your +extension for, but it says nothing about where the implementations live. An +extension declaring commands this way (or omitting the `commands` key +altogether) is loaded the old way: the CLI `require()`s the package's main entry +on **every** invocation and expects the module's top-level code to register +everything through the injector global. + +This path remains supported for published extensions, but it is tracked for +eventual deprecation and new extensions should not use it — declare the map +instead. Run any command with `--log trace` to see which installed extensions +still rely on it, or set `NS_DEPRECATIONS=warn` to have those reports printed +as warnings. + +Writing a command module +------------------------ + +The recommended shape is a module exporting a `defineCommand` definition (see +[defining-commands.md](defining-commands.md)) — the CLI adapts and registers it +under the manifest key when the command is first executed, and the module needs +no registration side effects at all: + +```js +// dist/commands/hello-world.js +const { defineCommand, inject } = require("nativescript/contracts"); + +module.exports = defineCommand({ + name: "hello|world", + arguments: "any", + async run(ctx) { + inject("logger").info(`Hello, ${ctx.args[0] || "world"}!`); + }, +}); +``` + +`inject()` resolves a CLI service against the injector running the command, and +works anywhere inside `run` up to the first `await`. It is why the peer +dependency above matters: with a second copy of the CLI installed alongside your +extension, `inject()` warns and points at the duplicate. + +A definition exported as `module.exports.default` (what a TypeScript or ESM +build emits) is picked up too. + +Legacy modules — command classes that register themselves at load time through +the injector global, with parameter-name constructor injection — keep working +when a manifest entry points at them, so existing extensions can adopt the map +without rewriting their commands. Both of those mechanisms are deprecated +(see [dependency-injection.md](dependency-injection.md)); write new modules as +definitions. + +If a module named by a manifest entry neither exports a definition nor registers +the command itself, executing that command fails with an error naming the +extension, the command and the module — the entry points at the wrong file, or +the file is not doing what the entry promises. + +Command names +------------- + +Command names use `|` to express hierarchy, so `"hello|world"` is invoked as +`ns hello world`. Prefixing the last segment with `*` marks a default +subcommand: `"hello|*default"` runs both for `ns hello default` and for a bare +`ns hello`. Names must be lower case — the CLI matches what the user typed in +lower case, so a key with an upper-case letter could never be reached, and is +rejected with a warning. + +**The manifest key decides how a command is invoked.** It has to: the CLI routes +`ns hello world` to your module before that module has been loaded, so the key +is the only name it can know. A `name` inside the definition is metadata — it is +what `registerCommandDefinition` uses when a module registers itself, and it is +useful documentation, but a manifest entry overrides it. If the two disagree the +CLI warns, naming both, and runs the command under the manifest key. + +An alias is a second entry pointing at the same module: + +```json +{ + "nativescript": { + "commands": { + "hello|world": "./dist/commands/hello-world.js", + "hello|w": "./dist/commands/hello-world.js" + } + } +} +``` + +Both names route to the same module, which is loaded once. + +When two extensions want the same command +----------------------------------------- + +The first extension to claim a command name keeps it; later claimants are +reported with a warning naming both extensions and the command, and their entry +is skipped. A name the CLI itself provides is never taken over — the extension +is told the command is already provided by the CLI. + +"First" is the order extensions are loaded in, which is the order they appear in +the `dependencies` of the profile directory's `extensions/package.json` — npm +keeps that alphabetically sorted, so in practice the alphabetically first +extension name wins. The exception is `ns extension install`: that invocation +loads the freshly installed extension after all the others, so a conflict it +would win on the next invocation goes the other way that one time. + +Suggesting an extension for an unknown command +---------------------------------------------- + +When a user types a command the CLI does not know, it searches npm for packages +carrying the `nativescript:extension` keyword, reads the `nativescript.commands` +key of each candidate's published `package.json`, and matches it against the +words the user typed — longest match first, so `ns valid command with args` +matches a declared `valid|command|with` before `valid|command`. A declared +default command also matches its short form: an extension declaring +`hello|*default` is suggested for a bare `ns hello`. + +Both manifest shapes participate in this matching. If a match is found, the CLI +tells the user which extension provides the command and how to install it: + +```text +The command hello world is registered in extension nativescript-hello. +You can install it by executing 'ns extension install nativescript-hello' +``` + +Documentation +------------- + +Point the `docs` key of the `nativescript` key at a directory of `.md` files to +have the CLI's help system pick up the help for your commands. + +```json +{ + "nativescript": { + "docs": "./docs", + "commands": { + "hello|world": "./dist/commands/hello-world.js" + } + } +} +``` diff --git a/lib/android-tools-info.ts b/lib/android-tools-info.ts index 5ceb095968..afd46f56ab 100644 --- a/lib/android-tools-info.ts +++ b/lib/android-tools-info.ts @@ -17,7 +17,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private $errors: IErrors, private $logger: ILogger, private $options: IOptions, - protected $staticConfig: Config.IStaticConfig + protected $staticConfig: Config.IStaticConfig, ) {} @cache() @@ -29,18 +29,18 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { infoData.androidHomeEnvVar = androidToolsInfo.androidHome; infoData.compileSdkVersion = this.getCompileSdkVersion( infoData.installedTargets, - infoData.compileSdkVersion + infoData.compileSdkVersion, ); infoData.targetSdkVersion = this.getTargetSdk(infoData.compileSdkVersion); infoData.generateTypings = this.shouldGenerateTypings(); this.$logger.trace( "Installed Android Targets are: ", - infoData.installedTargets + infoData.installedTargets, ); this.$logger.trace( "Selected buildToolsVersion is:", - infoData.buildToolsVersion + infoData.buildToolsVersion, ); return infoData; @@ -55,7 +55,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { androidToolsInfo .validateInfo({ projectDir: options.projectDir }) .map((warning) => - this.printMessage(warning.warning, showWarningsAsErrors) + this.printMessage(warning.warning, showWarningsAsErrors), ).length > 0; if (options && options.validateTargetSdk) { @@ -78,7 +78,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { projectDir: options.projectDir, }) .map((warning) => - this.printMessage(warning.warning, options.showWarningsAsErrors) + this.printMessage(warning.warning, options.showWarningsAsErrors), ).length > 0; if (!detectedErrors) { @@ -95,7 +95,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { public validateJavacVersion( installedJavacVersion: string, - options?: IAndroidToolsInfoOptions + options?: IAndroidToolsInfoOptions, ): boolean { const showWarningsAsErrors = options && options.showWarningsAsErrors; @@ -103,7 +103,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { androidToolsInfo .validateJavacVersion(installedJavacVersion) .map((warning) => - this.printMessage(warning.warning, showWarningsAsErrors) + this.printMessage(warning.warning, showWarningsAsErrors), ).length > 0 ); } @@ -118,8 +118,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { `Error while executing '${path.join( androidToolsInfo.androidHome, "platform-tools", - "adb" - )} help'. Error is: ${err.message}` + "adb", + )} help'. Error is: ${err.message}`, ); } @@ -128,7 +128,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { @cache() public validateAndroidHomeEnvVariable( - options?: IAndroidToolsInfoOptions + options?: IAndroidToolsInfoOptions, ): boolean { const showWarningsAsErrors = options && options.showWarningsAsErrors; @@ -136,7 +136,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { androidToolsInfo .validateAndroidHomeEnvVariable() .map((warning) => - this.printMessage(warning.warning, showWarningsAsErrors) + this.printMessage(warning.warning, showWarningsAsErrors), ).length > 0 ); } @@ -163,15 +163,30 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getCompileSdkVersion( installedTargets: string[], - latestCompileSdk: number + latestCompileSdk: number, ): number { const userSpecifiedCompileSdk = this.$options.compileSdk; if (userSpecifiedCompileSdk) { const androidCompileSdk = `${androidToolsInfo.ANDROID_TARGET_PREFIX}-${userSpecifiedCompileSdk}`; - if (!_.includes(installedTargets, androidCompileSdk)) { + // SDK platforms newer than android-36 may install into directories named + // "android-." (e.g. "android-37.0") with no plain + // "android-" directory, so installed targets are matched on their + // API level rather than the exact directory name. + const isTargetInstalled = _.some(installedTargets, (target) => { + if (target === androidCompileSdk) { + return true; + } + + const targetMatch = target.match(/^android-(\d+)(?:\.\d+)?$/); + return ( + targetMatch && + parseInt(targetMatch[1], 10) === userSpecifiedCompileSdk + ); + }); + if (!isTargetInstalled) { this.$errors.fail( - `You have specified '${userSpecifiedCompileSdk}' for compile sdk, but it is not installed on your system.` + `You have specified '${userSpecifiedCompileSdk}' for compile sdk, but it is not installed on your system.`, ); } diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 3281d84930..634c916e47 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -60,6 +60,7 @@ injector.require("iOSProvisionService", "./services/ios-provision-service"); injector.require("xcconfigService", "./services/xcconfig-service"); injector.require("iOSSigningService", "./services/ios/ios-signing-service"); injector.require("spmService", "./services/ios/spm-service"); +injector.require("spmPbxprojService", "./services/ios/spm-pbxproj-service"); injector.require( "xcodebuildArgsService", "./services/ios/xcodebuild-args-service", @@ -202,12 +203,16 @@ injector.requireCommand("deploy", "./commands/deploy"); injector.requireCommand("embed", "./commands/embedding/embed"); injector.require("testExecutionService", "./services/test-execution-service"); +injector.require( + "vitestExecutionService", + "./services/vitest-execution-service", +); injector.requireCommand("dev-test|android", "./commands/test"); injector.requireCommand("dev-test|ios", "./commands/test"); injector.requireCommand("test|android", "./commands/test"); injector.requireCommand("test|ios", "./commands/test"); -// injector.requireCommand("test|vision", "./commands/test"); -// injector.requireCommand("test|visionos", "./commands/test"); +injector.requireCommand("test|vision", "./commands/test"); +injector.requireCommand("test|visionos", "./commands/test"); injector.requireCommand("test|init", "./commands/test-init"); injector.requireCommand("dev-generate-help", "./commands/generate-help"); @@ -304,10 +309,6 @@ injector.require("deployCommandHelper", "./helpers/deploy-command-helper"); injector.require("platformCommandHelper", "./helpers/platform-command-helper"); injector.require("optionsTracker", "./helpers/options-track-helper"); -injector.requirePublicClass( - "localBuildService", - "./services/local-build-service", -); injector.require("LiveSyncSocket", "./services/livesync/livesync-socket"); injector.requirePublicClass( "androidLivesyncTool", @@ -407,25 +408,21 @@ injector.require( injector.require("hmrStatusService", "./services/hmr-status-service"); injector.require("pacoteService", "./services/pacote-service"); -injector.require( - "qrCodeTerminalService", - "./services/qr-code-terminal-service", -); injector.require( "testInitializationService", "./services/test-initialization-service", ); -injector.require( - "networkConnectivityValidator", - "./helpers/network-connectivity-validator", -); injector.requirePublic("cleanupService", "./services/cleanup-service"); injector.require( "bundlerCompilerService", "./services/bundler/bundler-compiler-service", ); +injector.require( + "viteHmrPortService", + "./services/bundler/vite-hmr-port-service", +); injector.require( "applePortalSessionService", @@ -479,5 +476,5 @@ injector.requireCommand( ], "./commands/native-add", ); -injector.requireCommand(["widget", "widget|ios"], "./commands/widget"); +injector.requireCommand(["widget|ios"], "./commands/widget"); require("./key-commands/bootstrap"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index f3c3c1adb3..7216e8a3fc 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -13,7 +13,8 @@ import { import { IPlatformsDataService } from "../definitions/platform"; import { IBuildController, IBuildDataService } from "../definitions/build"; import { IMigrateController } from "../definitions/migrate"; -import { IErrors, OptionType } from "../common/declarations"; +import { IErrors } from "../common/declarations"; +import { OptionType } from "../common/enums"; import { ICommandParameter, ICommand } from "../common/definitions/commands"; import { injector } from "../common/yok"; diff --git a/lib/commands/clean.ts b/lib/commands/clean.ts index 62a00bf8ce..d891ead677 100644 --- a/lib/commands/clean.ts +++ b/lib/commands/clean.ts @@ -6,6 +6,7 @@ import { IProjectCleanupResult, IProjectCleanupService, IProjectConfigService, + IProjectData, IProjectService, } from "../definitions/project"; @@ -83,6 +84,7 @@ export class CleanCommand implements ICommand { constructor( private $projectCleanupService: IProjectCleanupService, private $projectConfigService: IProjectConfigService, + private $projectData: IProjectData, private $terminalSpinnerService: ITerminalSpinnerService, private $projectService: IProjectService, private $prompter: IPrompter, @@ -108,7 +110,7 @@ export class CleanCommand implements ICommand { let pathsToClean = [ constants.HOOKS_DIR_NAME, - constants.PLATFORMS_DIR_NAME, + this.$projectData.getBuildRelativeDirectoryPath(), constants.NODE_MODULES_FOLDER_NAME, ]; diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index c7478ab09f..bd894e9019 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -17,6 +17,7 @@ import { ICleanupService } from "../definitions/cleanup-service"; import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import * as _ from "lodash"; +import { SystemWarningsSeverity } from "../definitions/system-warnings"; export class DebugPlatformCommand extends ValidatePlatformCommandBase diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index a06f6ea46f..2d2da7614f 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -10,12 +10,14 @@ import { IPlatformValidationService, IOptions } from "../declarations"; import { IPlatformsDataService } from "../definitions/platform"; import { IMigrateController } from "../definitions/migrate"; import { ICommand, ICommandParameter } from "../common/definitions/commands"; -import { OptionType, IErrors } from "../common/declarations"; +import { IErrors } from "../common/declarations"; +import { OptionType } from "../common/enums"; import { injector } from "../common/yok"; export class DeployOnDeviceCommand extends ValidatePlatformCommandBase - implements ICommand { + implements ICommand +{ public allowedParameters: ICommandParameter[] = []; public dashedOptions = { @@ -36,13 +38,13 @@ export class DeployOnDeviceCommand private $mobileHelper: Mobile.IMobileHelper, $platformsDataService: IPlatformsDataService, private $deployCommandHelper: DeployCommandHelper, - private $migrateController: IMigrateController + private $migrateController: IMigrateController, ) { super( $options, $platformsDataService, $platformValidationService, - $projectData + $projectData, ); this.$projectData.initializeProjectData(); } diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index e726b02861..03323b9e6c 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -6,7 +6,7 @@ import { IOptions, IPlatformValidationService } from "../declarations"; import { IPlatformsDataService } from "../definitions/platform"; import { IMigrateController } from "../definitions/migrate"; import { ICommand, ICommandParameter } from "../common/definitions/commands"; -import { OptionType } from "../common/declarations"; +import { OptionType } from "../common/enums"; import { injector } from "../common/yok"; export class PrepareCommand @@ -22,8 +22,7 @@ export class PrepareCommand hasSensitiveValue: false, }, hmr: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, - - whatever: { + skipNative: { type: OptionType.Boolean, default: false, hasSensitiveValue: false, @@ -38,13 +37,13 @@ export class PrepareCommand public $platformCommandParameter: ICommandParameter, public $platformsDataService: IPlatformsDataService, public $prepareDataService: PrepareDataService, - public $migrateController: IMigrateController + public $migrateController: IMigrateController, ) { super( $options, $platformsDataService, $platformValidationService, - $projectData + $projectData, ); this.$projectData.initializeProjectData(); } @@ -55,7 +54,7 @@ export class PrepareCommand const prepareData = this.$prepareDataService.getPrepareData( this.$projectData.projectDir, platform, - this.$options + this.$options, ); await this.$prepareController.prepare(prepareData); } @@ -68,7 +67,7 @@ export class PrepareCommand this.$options.provision, this.$options.teamId, this.$projectData, - platform + platform, )); if (!this.$options.force) { diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 872267b260..64bbb9e8eb 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -12,6 +12,7 @@ const PREVIEW_CLI_PACKAGE = "@nativescript/preview-cli"; export class PreviewCommand implements ICommand { allowedParameters: ICommandParameter[] = []; + skipOptionsValidation = true; constructor( private $logger: ILogger, @@ -19,7 +20,7 @@ export class PreviewCommand implements ICommand { private $projectData: IProjectData, private $packageManager: IPackageManager, private $childProcess: IChildProcess, - private $options: IOptions + private $options: IOptions, ) {} private getPreviewCLIPath(): string { @@ -37,7 +38,7 @@ export class PreviewCommand implements ICommand { { "save-dev": true, "save-exact": true, - } as any + } as any, ); } @@ -78,7 +79,7 @@ export class PreviewCommand implements ICommand { color.cyan(" ./node_modules/.bin/preview-cli"), "", "And if you are still having issues, try again - or reach out on Discord/open an issue on GitHub.", - ].join("\n") + ].join("\n"), ); this.$errors.fail("Running preview failed."); @@ -93,7 +94,7 @@ export class PreviewCommand implements ICommand { [previewCLIBinPath, ...commandArgs], { stdio: "inherit", - } + }, ); } diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 32b55ad04c..8cf06ebd4e 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -26,6 +26,70 @@ class TestInitCommand implements ICommand { mocha: ["chai"], }; + /** + * Android blocks cleartext traffic by default (API 28+), which would + * reject the runner's ws:// connection to the host. Scope the exception + * to the emulator loopback alias and adb-reverse loopback only. + */ + private ensureAndroidNetworkSecurityConfig(bufferedLogs: string[]): void { + const manifestPath = path.join( + this.$projectData.appResourcesDirectoryPath, + "Android", + "src", + "main", + "AndroidManifest.xml", + ); + if (!this.$fs.exists(manifestPath)) { + bufferedLogs.push( + color.yellow( + "Could not locate App_Resources/Android/src/main/AndroidManifest.xml. For Android test runs, allow cleartext traffic to 10.0.2.2 and 127.0.0.1 via a network security config.", + ), + ); + return; + } + + const manifestContent = this.$fs.readText(manifestPath); + if (manifestContent.indexOf("networkSecurityConfig") !== -1) { + bufferedLogs.push( + color.yellow( + "AndroidManifest.xml already sets android:networkSecurityConfig — make sure it permits cleartext traffic to 10.0.2.2 and 127.0.0.1 for test runs.", + ), + ); + return; + } + + const xmlDirectory = path.join( + this.$projectData.appResourcesDirectoryPath, + "Android", + "src", + "main", + "res", + "xml", + ); + this.$fs.ensureDirectoryExists(xmlDirectory); + const securityConfigPath = path.join(xmlDirectory, "network_security.xml"); + if (!this.$fs.exists(securityConfigPath)) { + this.$fs.copyFile( + this.$resources.resolvePath("test/network_security.xml"), + securityConfigPath, + ); + bufferedLogs.push( + `Added ${color.yellow("App_Resources/Android/src/main/res/xml/network_security.xml")}`, + ); + } + + this.$fs.writeFile( + manifestPath, + manifestContent.replace( + / !moduleToInstall.projectType || - moduleToInstall.projectType === projectFilesExtension + moduleToInstall.projectType === projectFilesExtension, ); for (const mod of modulesToInstall) { let moduleToInstall = mod.name; moduleToInstall += `@${mod.version}`; await this.$packageManager.install(moduleToInstall, projectDir, { - "save-dev": true, + // Packages with native code must land in "dependencies" — the CLI + // integrates plugin platform files (pods, aars) only from there. + ...(mod.saveInDependencies ? { save: true } : { "save-dev": true }), "save-exact": true, optional: false, disableNpmInstall: this.$options.disableNpmInstall, @@ -97,16 +162,40 @@ class TestInitCommand implements ICommand { const modulePackageJsonContent = this.$fs.readJson(modulePackageJsonPath); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; + const modulePeerDependenciesMeta = + modulePackageJsonContent.peerDependenciesMeta || {}; + const projectPackageJson = this.$fs.readJson( + path.join(projectDir, "package.json"), + ); + const installedProjectDependencies = { + ...projectPackageJson.dependencies, + ...projectPackageJson.devDependencies, + }; for (const peerDependency in modulePeerDependencies) { const isPeerDependencyExcluded = _.includes( mod.excludedPeerDependencies, - peerDependency + peerDependency, ); if (isPeerDependencyExcluded) { continue; } + if ( + modulePeerDependenciesMeta[peerDependency] && + modulePeerDependenciesMeta[peerDependency].optional + ) { + continue; + } + + // Reinstalling an already-declared package would move it to + // devDependencies — for packages with native code (e.g. + // @nativescript/core) that strips their platform files from the + // native build. + if (installedProjectDependencies[peerDependency]) { + continue; + } + const dependencyVersion = modulePeerDependencies[peerDependency] || "*"; // catch errors when a peerDependency is already installed @@ -122,7 +211,7 @@ class TestInitCommand implements ICommand { frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, - } + }, ); } catch (e) { this.$logger.error(e.message); @@ -130,10 +219,16 @@ class TestInitCommand implements ICommand { } } - await this.$pluginsService.add( - "@nativescript/unit-test-runner", - this.$projectData - ); + const isVitest = frameworkToInstall === "vitest"; + + if (!isVitest) { + // The Karma client only exists in the v4 line — v5+ is Vitest-only, so + // an unpinned install would break these setups once v5 is `latest`. + await this.$pluginsService.add( + "@nativescript/unit-test-runner@^4.0.0", + this.$projectData, + ); + } this.$logger.clearScreen(); @@ -142,11 +237,11 @@ class TestInitCommand implements ICommand { const testsDir = path.join(this.$projectData.appDirectoryPath, "tests"); const projectTestsDir = path.relative( this.$projectData.projectDir, - testsDir + testsDir, ); const relativeTestsDir = path.relative( this.$projectData.appDirectoryPath, - testsDir + testsDir, ); let shouldCreateSampleTests = true; if (this.$fs.exists(testsDir)) { @@ -157,80 +252,135 @@ class TestInitCommand implements ICommand { `Note: The "${projectTestsDir}" directory already exists, will not create example tests in the project.`, `You may create "${specFilenamePattern}" files anywhere you'd like.`, "", - ].join("\n") - ) + ].join("\n"), + ), ); shouldCreateSampleTests = false; } this.$fs.ensureDirectoryExists(testsDir); - const frameworks = [frameworkToInstall] - .concat(this.karmaConfigAdditionalFrameworks[frameworkToInstall] || []) - .map((fw) => `'${fw}'`) - .join(", "); - const testFiles = `'${fromWindowsRelativePathToUnix( - relativeTestsDir - )}/**/*${projectFilesExtension}'`; - const karmaConfTemplate = this.$resources.readText("test/karma.conf.js"); - const karmaConf = _.template(karmaConfTemplate)({ - frameworks, - testFiles, - basePath: this.$projectData.getAppDirectoryRelativePath(), - }); - - this.$fs.writeFile(path.join(projectDir, "karma.conf.js"), karmaConf); + if (isVitest) { + const vitestConfigResourcePath = this.$resources.resolvePath( + "test/vitest.config.mts", + ); + this.$fs.copyFile( + vitestConfigResourcePath, + path.join(projectDir, "vitest.config.mts"), + ); + bufferedLogs.push(`Added/replaced ${color.yellow("vitest.config.mts")}`); + this.ensureAndroidNetworkSecurityConfig(bufferedLogs); + } else { + const frameworks = [frameworkToInstall] + .concat(this.karmaConfigAdditionalFrameworks[frameworkToInstall] || []) + .map((fw) => `'${fw}'`) + .join(", "); + const testFiles = `'${fromWindowsRelativePathToUnix( + relativeTestsDir, + )}/**/*${projectFilesExtension}'`; + const karmaConfTemplate = this.$resources.readText("test/karma.conf.js"); + const karmaConf = _.template(karmaConfTemplate)({ + frameworks, + testFiles, + basePath: this.$projectData.getAppDirectoryRelativePath(), + }); + + this.$fs.writeFile(path.join(projectDir, "karma.conf.js"), karmaConf); + } const exampleFilePath = this.$resources.resolvePath( - `test/example.${frameworkToInstall}${projectFilesExtension}` + `test/example.${frameworkToInstall}${projectFilesExtension}`, ); const targetExampleTestPath = path.join( testsDir, - `example.spec${projectFilesExtension}` + `example.spec${projectFilesExtension}`, ); if (shouldCreateSampleTests && this.$fs.exists(exampleFilePath)) { this.$fs.copyFile(exampleFilePath, targetExampleTestPath); const targetExampleTestRelativePath = path.relative( projectDir, - targetExampleTestPath + targetExampleTestPath, ); bufferedLogs.push( - `Added example test: ${color.yellow(targetExampleTestRelativePath)}` + `Added example test: ${color.yellow(targetExampleTestRelativePath)}`, ); } // test main entry const testMainResourcesPath = this.$resources.resolvePath( - `test/test-main${projectFilesExtension}` + isVitest + ? `test/test-main.vitest${projectFilesExtension}` + : `test/test-main${projectFilesExtension}`, ); const testMainPath = path.join( this.$projectData.appDirectoryPath, - `test${projectFilesExtension}` + `test${projectFilesExtension}`, ); if (!this.$fs.exists(testMainPath)) { this.$fs.copyFile(testMainResourcesPath, testMainPath); const testMainRelativePath = path.relative(projectDir, testMainPath); bufferedLogs.push( - `Main test entrypoint created: ${color.yellow(testMainRelativePath)}` + `Main test entrypoint created: ${color.yellow(testMainRelativePath)}`, ); } - const testTsConfigTemplate = this.$resources.readText( - "test/tsconfig.spec.json" - ); - const testTsConfig = _.template(testTsConfigTemplate)({ - basePath: this.$projectData.getAppDirectoryRelativePath(), - }); + if (!isVitest || projectFilesExtension === ".ts") { + const testTsConfigTemplate = this.$resources.readText( + "test/tsconfig.spec.json", + ); + const testTsConfig = _.template(testTsConfigTemplate)({ + basePath: this.$projectData.getAppDirectoryRelativePath(), + }); - this.$fs.writeFile( - path.join(projectDir, "tsconfig.spec.json"), - testTsConfig - ); - bufferedLogs.push(`Added/replaced ${color.yellow("tsconfig.spec.json")}`); + this.$fs.writeFile( + path.join(projectDir, "tsconfig.spec.json"), + testTsConfig, + ); + bufferedLogs.push(`Added/replaced ${color.yellow("tsconfig.spec.json")}`); + } const greyDollarSign = color.grey("$"); + const closingNotes = isVitest + ? [ + color.yellow( + `Note: emulator/simulator test runs connect over the local loopback. When testing on a physical Android device, keep it connected over USB (adb reverse is set up automatically); for a physical iOS or visionOS device, pass a reachable 'url' to the coordinator in your test entry.`, + ), + "", + "", + `You can now run your tests:`, + "", + ` ${greyDollarSign} ${color.green("ns test ios")}`, + ` ${greyDollarSign} ${color.green("ns test android")}`, + ` ${greyDollarSign} ${color.green("ns test visionos")}`, + "", + `or directly through Vitest (editor extensions, CI):`, + "", + ` ${greyDollarSign} ${color.green("NS_PLATFORM=ios npx vitest run")}`, + "", + ] + : [ + color.yellow( + `Note: @nativescript/unit-test-runner was included in "dependencies" as a convenience to automatically adjust your app's Info.plist on iOS and AndroidManifest.xml on Android to ensure the socket connects properly.`, + ), + "", + color.yellow( + `For production you may want to move to "devDependencies" and manage the settings yourself.`, + ), + "", + color.yellow( + `Karma-based unit testing is deprecated and will be removed in a future release. Consider '$ ns test init --framework vitest'.`, + ), + "", + "", + `You can now run your tests:`, + "", + ` ${greyDollarSign} ${color.green("ns test ios")}`, + ` ${greyDollarSign} ${color.green("ns test android")}`, + "", + ]; + this.$logger.info( [ [ @@ -241,21 +391,8 @@ class TestInitCommand implements ICommand { "", ...bufferedLogs, "", - color.yellow( - `Note: @nativescript/unit-test-runner was included in "dependencies" as a convenience to automatically adjust your app's Info.plist on iOS and AndroidManifest.xml on Android to ensure the socket connects properly.` - ), - "", - color.yellow( - `For production you may want to move to "devDependencies" and manage the settings yourself.` - ), - "", - "", - `You can now run your tests:`, - "", - ` ${greyDollarSign} ${color.green("ns test ios")}`, - ` ${greyDollarSign} ${color.green("ns test android")}`, - "", - ].join("\n") + ...closingNotes, + ].join("\n"), ); } } diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 2e12127477..646af87e3e 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -3,18 +3,21 @@ import { ANDROID_RELEASE_BUILD_ERROR_MESSAGE, ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE, } from "../constants"; -import { IProjectData, ITestExecutionService } from "../definitions/project"; +import { + IProjectData, + ITestExecutionService, + IVitestExecutionService, +} from "../definitions/project"; import { IOptions } from "../declarations"; import { IPlatformEnvironmentRequirements } from "../definitions/platform"; import { IMigrateController } from "../definitions/migrate"; import { ICommandParameter, ICommand } from "../common/definitions/commands"; import { - OptionType, IAnalyticsService, IErrors, IDictionary, - ErrorCodes, } from "../common/declarations"; +import { ErrorCodes, OptionType } from "../common/enums"; import { ICleanupService } from "../definitions/cleanup-service"; import { injector } from "../common/yok"; @@ -27,6 +30,7 @@ abstract class TestCommandBase { protected abstract platform: string; protected abstract $projectData: IProjectData; protected abstract $testExecutionService: ITestExecutionService; + protected abstract $vitestExecutionService: IVitestExecutionService; protected abstract $analyticsService: IAnalyticsService; protected abstract $options: IOptions; protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; @@ -35,8 +39,22 @@ abstract class TestCommandBase { protected abstract $liveSyncCommandHelper: ILiveSyncCommandHelper; protected abstract $devicesService: Mobile.IDevicesService; protected abstract $migrateController: IMigrateController; + protected abstract $logger: ILogger; public async execute(args: string[]): Promise { + if (this.$vitestExecutionService.isVitestProject(this.$projectData)) { + await this.$vitestExecutionService.startTestRun( + this.platform, + this.$projectData, + ); + process.exit(0); + } + + this.$logger.warn( + "Karma-based unit testing is deprecated and will be removed in a future release. " + + "Re-initialize your tests with '$ ns test init --framework vitest' to migrate.", + ); + let devices = []; if (this.$options.debugBrk) { await this.$devicesService.initialize({ @@ -47,19 +65,18 @@ abstract class TestCommandBase { sdk: this.$options.sdk, }); - const selectedDeviceForDebug = await this.$devicesService.pickSingleDevice( - { + const selectedDeviceForDebug = + await this.$devicesService.pickSingleDevice({ onlyEmulators: this.$options.emulator, onlyDevices: this.$options.forDevice, deviceId: this.$options.device, - } - ); + }); devices = [selectedDeviceForDebug]; // const debugData = this.getDebugData(platform, projectData, deployOptions, { device: selectedDeviceForDebug.deviceInfo.identifier }); // await this.$debugService.debug(debugData, this.$options); } else { devices = await this.$liveSyncCommandHelper.getDeviceInstances( - this.platform + this.platform, ); } @@ -69,25 +86,26 @@ abstract class TestCommandBase { this.$options.env.unitTesting = true; const liveSyncInfo = this.$liveSyncCommandHelper.getLiveSyncData( - this.$projectData.projectDir + this.$projectData.projectDir, ); const deviceDebugMap: IDictionary = {}; devices.forEach( (device) => - (deviceDebugMap[device.deviceInfo.identifier] = this.$options.debugBrk) + (deviceDebugMap[device.deviceInfo.identifier] = this.$options.debugBrk), ); - const deviceDescriptors = await this.$liveSyncCommandHelper.createDeviceDescriptors( - devices, - this.platform, - { deviceDebugMap } - ); + const deviceDescriptors = + await this.$liveSyncCommandHelper.createDeviceDescriptors( + devices, + this.platform, + { deviceDebugMap }, + ); await this.$testExecutionService.startKarmaServer( this.platform, liveSyncInfo, - deviceDescriptors + deviceDescriptors, ); // if we got here, it means karma exited with exit code 0 (success) process.exit(0); @@ -100,7 +118,7 @@ abstract class TestCommandBase { // because the Runtime does not watch for the `/data/local/tmp-livesync-in-progress` file deletion. // The App is closing itself after each test execution and the bug will be reproducible on each LiveSync. this.$errors.fail( - "The `--hmr` option is not supported for this command." + "The `--hmr` option is not supported for this command.", ); } @@ -112,23 +130,35 @@ abstract class TestCommandBase { this.$projectData.initializeProjectData(); this.$analyticsService.setShouldDispose( - this.$options.justlaunch || !this.$options.watch + this.$options.justlaunch || !this.$options.watch, ); this.$cleanupService.setShouldDispose( - this.$options.justlaunch || !this.$options.watch + this.$options.justlaunch || !this.$options.watch, ); - const output = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements( - { + const output = + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform: this.platform, projectDir: this.$projectData.projectDir, options: this.$options, + }); + + if (this.$vitestExecutionService.isVitestProject(this.$projectData)) { + const canStartTestRun = this.$vitestExecutionService.canStartTestRun( + this.$projectData, + ); + if (!canStartTestRun) { + this.$errors.fail({ + formatStr: + "Error: In order to run unit tests, your project must already be configured by running $ ns test init.", + errorCode: ErrorCodes.TESTS_INIT_REQUIRED, + }); } - ); + return output.canExecute && canStartTestRun; + } - const canStartKarmaServer = await this.$testExecutionService.canStartKarmaServer( - this.$projectData - ); + const canStartKarmaServer = + await this.$testExecutionService.canStartKarmaServer(this.$projectData); if (!canStartKarmaServer) { this.$errors.fail({ formatStr: @@ -147,6 +177,7 @@ class TestAndroidCommand extends TestCommandBase implements ICommand { constructor( protected $projectData: IProjectData, protected $testExecutionService: ITestExecutionService, + protected $vitestExecutionService: IVitestExecutionService, protected $analyticsService: IAnalyticsService, protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, @@ -154,7 +185,8 @@ class TestAndroidCommand extends TestCommandBase implements ICommand { protected $cleanupService: ICleanupService, protected $liveSyncCommandHelper: ILiveSyncCommandHelper, protected $devicesService: Mobile.IDevicesService, - protected $migrateController: IMigrateController + protected $migrateController: IMigrateController, + protected $logger: ILogger, ) { super(); } @@ -188,6 +220,7 @@ class TestIosCommand extends TestCommandBase implements ICommand { constructor( protected $projectData: IProjectData, protected $testExecutionService: ITestExecutionService, + protected $vitestExecutionService: IVitestExecutionService, protected $analyticsService: IAnalyticsService, protected $options: IOptions, protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, @@ -195,11 +228,63 @@ class TestIosCommand extends TestCommandBase implements ICommand { protected $cleanupService: ICleanupService, protected $liveSyncCommandHelper: ILiveSyncCommandHelper, protected $devicesService: Mobile.IDevicesService, - protected $migrateController: IMigrateController + protected $migrateController: IMigrateController, + protected $logger: ILogger, ) { super(); } } +class TestVisionOSCommand extends TestIosCommand { + protected platform = "visionOS"; + + // The injector discovers dependencies by parsing constructor source text, + // so an inherited constructor would resolve to zero dependencies. + constructor( + protected $projectData: IProjectData, + protected $testExecutionService: ITestExecutionService, + protected $vitestExecutionService: IVitestExecutionService, + protected $analyticsService: IAnalyticsService, + protected $options: IOptions, + protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, + protected $errors: IErrors, + protected $cleanupService: ICleanupService, + protected $liveSyncCommandHelper: ILiveSyncCommandHelper, + protected $devicesService: Mobile.IDevicesService, + protected $migrateController: IMigrateController, + protected $logger: ILogger, + ) { + super( + $projectData, + $testExecutionService, + $vitestExecutionService, + $analyticsService, + $options, + $platformEnvironmentRequirements, + $errors, + $cleanupService, + $liveSyncCommandHelper, + $devicesService, + $migrateController, + $logger, + ); + } + + async canExecute(args: string[]): Promise { + this.$projectData.initializeProjectData(); + // The Karma runner (v4 line) never supported visionOS — only the Vitest + // path can drive it. + if (!this.$vitestExecutionService.isVitestProject(this.$projectData)) { + this.$errors.fail( + "visionOS unit testing requires the Vitest runner. Run '$ ns test init --framework vitest' to configure your project.", + ); + } + + return super.canExecute(args); + } +} + injector.registerCommand("test|android", TestAndroidCommand); injector.registerCommand("test|ios", TestIosCommand); +injector.registerCommand("test|vision", TestVisionOSCommand); +injector.registerCommand("test|visionos", TestVisionOSCommand); diff --git a/lib/commands/typings.ts b/lib/commands/typings.ts index 9159e6f5f5..d2e296fa91 100644 --- a/lib/commands/typings.ts +++ b/lib/commands/typings.ts @@ -161,8 +161,7 @@ export class TypingsCommand implements ICommand { ); const dtsGeneratorPath = path.resolve( - this.$projectData.projectDir, - "platforms", + this.$projectData.platformsDir, "android", "build-tools", "dts-generator.jar", diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 5a12e69fe6..5d511d9a6e 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -934,5 +934,6 @@ declare class AppleWidgetUtils extends NSObject { } } -injector.registerCommand(["widget"], WidgetCommand); +// No flat "widget": the subcommand registration below synthesizes the parent +// dispatcher, and WidgetCommand serves as WidgetIOSCommand's base class. injector.registerCommand(["widget|ios"], WidgetIOSCommand); diff --git a/lib/common/bootstrap.ts b/lib/common/bootstrap.ts index 9905f452b9..287586c134 100644 --- a/lib/common/bootstrap.ts +++ b/lib/common/bootstrap.ts @@ -9,7 +9,6 @@ injector.requirePublic("fs", "./file-system"); injector.require("hostInfo", "./host-info"); injector.require("osInfo", "./os-info"); -injector.require("dispatcher", "./dispatchers"); injector.require("commandDispatcher", "./dispatchers"); injector.require("resources", "./resource-loader"); @@ -42,15 +41,15 @@ injector.requireCommand("autocomplete|status", "./commands/autocompletion"); injector.requireCommand( ["device|*list", "devices|*list"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand( ["device|android", "devices|android"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand( ["device|ios", "devices|ios"], - "./commands/device/list-devices" + "./commands/device/list-devices", ); injector.requireCommand("device|log", "./commands/device/device-log-stream"); @@ -58,11 +57,11 @@ injector.requireCommand("device|run", "./commands/device/run-application"); injector.requireCommand("device|stop", "./commands/device/stop-application"); injector.requireCommand( "device|list-applications", - "./commands/device/list-applications" + "./commands/device/list-applications", ); injector.requireCommand( "device|uninstall", - "./commands/device/uninstall-application" + "./commands/device/uninstall-application", ); injector.requireCommand("device|list-files", "./commands/device/list-files"); injector.requireCommand("device|get-file", "./commands/device/get-file"); @@ -70,84 +69,83 @@ injector.requireCommand("device|put-file", "./commands/device/put-file"); injector.require( "iosDeviceOperations", - "./mobile/ios/device/ios-device-operations" + "./mobile/ios/device/ios-device-operations", ); injector.require("deviceDiscovery", "./mobile/mobile-core/device-discovery"); injector.require( "iOSDeviceDiscovery", - "./mobile/mobile-core/ios-device-discovery" + "./mobile/mobile-core/ios-device-discovery", ); injector.require( "iOSSimulatorDiscovery", - "./mobile/mobile-core/ios-simulator-discovery" + "./mobile/mobile-core/ios-simulator-discovery", ); injector.require( "androidDeviceDiscovery", - "./mobile/mobile-core/android-device-discovery" + "./mobile/mobile-core/android-device-discovery", ); injector.require( "androidEmulatorDiscovery", - "./mobile/mobile-core/android-emulator-discovery" + "./mobile/mobile-core/android-emulator-discovery", ); injector.require("iOSDevice", "./mobile/ios/device/ios-device"); injector.require( "iOSDeviceProductNameMapper", - "./mobile/ios/ios-device-product-name-mapper" + "./mobile/ios/ios-device-product-name-mapper", ); injector.require("androidDevice", "./mobile/android/android-device"); injector.require("adb", "./mobile/android/android-debug-bridge"); injector.require( "androidDebugBridgeResultHandler", - "./mobile/android/android-debug-bridge-result-handler" + "./mobile/android/android-debug-bridge-result-handler", ); injector.require( "androidVirtualDeviceService", - "./mobile/android/android-virtual-device-service" + "./mobile/android/android-virtual-device-service", ); injector.require( "androidIniFileParser", - "./mobile/android/android-ini-file-parser" + "./mobile/android/android-ini-file-parser", ); injector.require( "androidGenymotionService", - "./mobile/android/genymotion/genymotion-service" + "./mobile/android/genymotion/genymotion-service", ); injector.require( "virtualBoxService", - "./mobile/android/genymotion/virtualbox-service" + "./mobile/android/genymotion/virtualbox-service", ); injector.require("logcatHelper", "./mobile/android/logcat-helper"); injector.require("iOSSimResolver", "./mobile/ios/simulator/ios-sim-resolver"); injector.require( "iOSSimulatorLogProvider", - "./mobile/ios/simulator/ios-simulator-log-provider" + "./mobile/ios/simulator/ios-simulator-log-provider", ); injector.require( "localToDevicePathDataFactory", - "./mobile/local-to-device-path-data-factory" + "./mobile/local-to-device-path-data-factory", ); injector.requirePublic( "devicesService", - "./mobile/mobile-core/devices-service" + "./mobile/mobile-core/devices-service", ); injector.requirePublic( "androidProcessService", - "./mobile/mobile-core/android-process-service" + "./mobile/mobile-core/android-process-service", ); injector.require("projectNameValidator", "./validators/project-name-validator"); injector.require( "androidEmulatorServices", - "./mobile/android/android-emulator-services" + "./mobile/android/android-emulator-services", ); injector.require( "iOSEmulatorServices", - "./mobile/ios/simulator/ios-emulator-services" + "./mobile/ios/simulator/ios-emulator-services", ); -injector.require("wp8EmulatorServices", "./mobile/wp8/wp8-emulator-services"); injector.require("autoCompletionService", "./services/auto-completion-service"); injector.requirePublic("settingsService", "./services/settings-service"); @@ -157,18 +155,18 @@ injector.require("mobileHelper", "./mobile/mobile-helper"); injector.require("emulatorHelper", "./mobile/emulator-helper"); injector.require( "devicePlatformsConstants", - "./mobile/device-platforms-constants" + "./mobile/device-platforms-constants", ); injector.require("helpService", "./services/help-service"); injector.require( "messageContractGenerator", - "./services/message-contract-generator" + "./services/message-contract-generator", ); injector.require("proxyService", "./services/proxy-service"); injector.requireCommand("dev-preuninstall", "./commands/preuninstall"); injector.requireCommand( "dev-generate-messages", - "./commands/generate-messages" + "./commands/generate-messages", ); injector.requireCommand("doctor|*all", "./commands/doctor"); injector.requireCommand("doctor|ios", "./commands/doctor"); @@ -190,5 +188,4 @@ injector.require("projectFilesManager", "./services/project-files-manager"); injector.require("xcodeSelectService", "./services/xcode-select-service"); injector.require("net", "./services/net-service"); -injector.require("qr", "./services/qr"); injector.require(["lockfile", "lockService"], "./services/lock-service"); diff --git a/lib/common/codeGeneration/code-entity.ts b/lib/common/codeGeneration/code-entity.ts index 95ddd1147f..c9d927cf5b 100644 --- a/lib/common/codeGeneration/code-entity.ts +++ b/lib/common/codeGeneration/code-entity.ts @@ -1,5 +1,4 @@ import * as _ from "lodash"; -import { injector } from "../yok"; import { CodeGeneration } from "./code-generation"; export enum CodeEntityType { @@ -22,7 +21,6 @@ export class Line implements CodeGeneration.ILine { return new Line(content); } } -injector.register("swaggerLine", Line); export class Block implements CodeGeneration.IBlock { public opener: string; @@ -55,4 +53,3 @@ export class Block implements CodeGeneration.IBlock { this.codeEntities.push(line); } } -injector.register("swaggerBlock", Block); diff --git a/lib/common/codeGeneration/code-printer.ts b/lib/common/codeGeneration/code-printer.ts index 53e1c772ba..64acf4a5e2 100644 --- a/lib/common/codeGeneration/code-printer.ts +++ b/lib/common/codeGeneration/code-printer.ts @@ -2,7 +2,6 @@ import * as _ from "lodash"; import { EOL } from "os"; import { CodeEntityType } from "./code-entity"; import { CodeGeneration } from "./code-generation"; -import { injector } from "../yok"; export class CodePrinter { private static INDENT_CHAR = "\t"; @@ -12,7 +11,7 @@ export class CodePrinter { public composeBlock( block: CodeGeneration.IBlock, - indentSize?: number + indentSize?: number, ): string { indentSize = indentSize === undefined ? 0 : indentSize; let content = this.getIndentation(indentSize); @@ -27,12 +26,12 @@ export class CodePrinter { if (codeEntity.codeEntityType === CodeEntityType.Line) { content += this.composeLine( codeEntity, - indentSize + 1 + indentSize + 1, ); } else if (codeEntity.codeEntityType === CodeEntityType.Block) { content += this.composeBlock( codeEntity, - indentSize + 1 + indentSize + 1, ); } }); @@ -59,4 +58,3 @@ export class CodePrinter { return content; } } -injector.register("swaggerCodePrinter", CodePrinter); diff --git a/lib/common/contracts/command-registry.ts b/lib/common/contracts/command-registry.ts new file mode 100644 index 0000000000..d6484f4e7d --- /dev/null +++ b/lib/common/contracts/command-registry.ts @@ -0,0 +1,82 @@ +import { Contract } from "../di/contract"; +import type { ICommand } from "../definitions/commands"; + +export interface DeferredCommandOptions { + /** + * Names the registrant in conflict and failure reports. Re-registering the + * same command under the same owner is a no-op rather than a conflict. + */ + owner: string; + /** Where the implementation comes from; named when loading it fails. */ + source: string; + /** + * Runs on first resolution of the command. It must leave a real resolver on + * the command name — by exporting a definition the caller registers, or by + * registering the command itself. + */ + load: () => void; +} + +/** Why a deferred registration did not take effect. */ +export type DeferredCommandRejection = + /** The name can never be dispatched; `detail` says why. */ + | { reason: "invalid-name"; detail: string } + /** Another owner registered the same command first. */ + | { reason: "claimed"; owner: string } + /** The CLI itself provides the command. */ + | { reason: "built-in" } + /** The name is in use as the dispatcher for subcommands under it. */ + | { reason: "subcommand-parent" } + /** + * The name's direct parent is a command of its own, so no dispatcher can be + * built for it and the name could never be reached. + */ + | { reason: "parent-is-command"; parent: string }; + +/** + * Outcome of a deferred registration. Callers branch on `rejection.reason` + * rather than on message text, so the wording of the report stays theirs. + */ +export interface DeferredCommandResult { + registered: boolean; + /** Set exactly when `registered` is false. */ + rejection?: DeferredCommandRejection; +} + +/** + * The command-registry face of the injector facade. Transitional contract: it + * mirrors what consumers call today, so that extracting the registry from the + * facade later is a provider swap for this token, not a consumer migration. + * Members slated for replacement keep their deprecation markers. + */ +@Contract({ name: "commandRegistry" }) +export abstract class CommandRegistry { + /** + * @deprecated Path-based command registration; use registerDeferredCommand, + * which routes without loading and reports conflicts structurally. + */ + abstract requireCommand(names: string | string[], file: string): void; + abstract registerCommand(names: string | string[], resolver: any): void; + /** + * Claims a command name for an owner without loading anything: routing — + * including the dispatcher of a hierarchical parent — is built from the name + * alone, and `load` runs only when that one command is resolved. + */ + abstract registerDeferredCommand( + name: string, + options: DeferredCommandOptions, + ): DeferredCommandResult; + abstract resolveCommand(name: string): ICommand; + abstract getRegisteredCommandsNames(includeDev: boolean): string[]; + abstract getChildrenCommandsNames(commandName: string): string[]; + abstract buildHierarchicalCommand( + parentCommandName: string, + commandLineArguments: string[], + ): any; + /** Side-effecting: fails with help output on a bad subcommand. */ + abstract isValidHierarchicalCommand( + commandName: string, + commandArguments: string[], + ): Promise; + abstract isDefaultCommand(commandName: string): boolean; +} diff --git a/lib/common/contracts/index.ts b/lib/common/contracts/index.ts new file mode 100644 index 0000000000..29e06c2109 --- /dev/null +++ b/lib/common/contracts/index.ts @@ -0,0 +1,15 @@ +// Internal subsystem contracts of the injector facade. Deliberately NOT +// re-exported from nativescript/contracts: promoting one to the public +// surface is a one-line decision that should be made per contract, not by +// default. Each token resolves to the facade itself until its subsystem is +// physically extracted — at which point the provider is swapped and consumers +// keep working unchanged. +export { CommandRegistry } from "./command-registry"; +export type { + DeferredCommandOptions, + DeferredCommandRejection, + DeferredCommandResult, +} from "./command-registry"; +export { KeyCommandRegistry } from "./key-command-registry"; +export { ModuleRegistry } from "./module-registry"; +export { PublicApiBuilder } from "./public-api-builder"; diff --git a/lib/common/contracts/key-command-registry.ts b/lib/common/contracts/key-command-registry.ts new file mode 100644 index 0000000000..f16e1ee97d --- /dev/null +++ b/lib/common/contracts/key-command-registry.ts @@ -0,0 +1,15 @@ +import { Contract } from "../di/contract"; +import type { IKeyCommand, IValidKeyName } from "../definitions/key-commands"; + +/** + * The key-command face of the injector facade (the `keyCommands.` namespace). + * Kept separate from CommandRegistry because the two registries are redesigned + * on different tracks. + */ +@Contract({ name: "keyCommandRegistry" }) +export abstract class KeyCommandRegistry { + abstract requireKeyCommand(name: IValidKeyName, file: string): void; + abstract registerKeyCommand(name: IValidKeyName, resolver: any): void; + abstract resolveKeyCommand(name: string): IKeyCommand; + abstract getRegisteredKeyCommandsNames(): string[]; +} diff --git a/lib/common/contracts/module-registry.ts b/lib/common/contracts/module-registry.ts new file mode 100644 index 0000000000..2ebca751ab --- /dev/null +++ b/lib/common/contracts/module-registry.ts @@ -0,0 +1,13 @@ +import { Contract } from "../di/contract"; + +/** + * @deprecated The lazy module-loader face of the injector facade — the + * require-time path map. Wholly replaced by provideLazy(); this contract + * exists so its remaining consumers are typed against exactly what they use + * until the bootstrap migrates. + */ +@Contract({ name: "moduleRegistry" }) +export abstract class ModuleRegistry { + abstract require(names: string | string[], file: string): void; + abstract overrideAlreadyRequiredModule: boolean; +} diff --git a/lib/common/contracts/public-api-builder.ts b/lib/common/contracts/public-api-builder.ts new file mode 100644 index 0000000000..6ddb51cc53 --- /dev/null +++ b/lib/common/contracts/public-api-builder.ts @@ -0,0 +1,13 @@ +import { Contract } from "../di/contract"; + +/** + * The public-API-builder face of the injector facade: the machinery behind + * `require('nativescript')`. Bound by the compatibility constraints of the + * published library surface; do not add new entries through it. + */ +@Contract({ name: "publicApiBuilder" }) +export abstract class PublicApiBuilder { + abstract requirePublic(names: string | string[], file: string): void; + abstract requirePublicClass(names: string | string[], file: string): void; + abstract publicApi: any; +} diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index 9102cdea65..df4a19a1fb 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -4,7 +4,12 @@ import { IEventActionData, IGoogleAnalyticsData, } from "./definitions/google-analytics"; -import * as child_process from "child_process"; +import type { DoctorService } from "../contracts/doctor-service"; +import type { ChildProcess } from "../contracts/child-process"; +import type { Errors } from "../contracts/errors"; +import type { FileSystem } from "../contracts/file-system"; +import type { HostInfo } from "../contracts/host-info"; +import type { HttpClient } from "../contracts/http-client"; // tslint:disable-next-line:interface-name interface Object { @@ -35,10 +40,6 @@ interface IiTunesConnectApplicationType { * Their values are the names of the methods in universnal-analytics that have to be called to track this type of data. * Also known as Hit Type: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#t */ -declare const enum GoogleAnalyticsDataType { - Page = "pageview", - Event = "event", -} /** * Descibes iTunes Connect applications @@ -166,13 +167,8 @@ declare module Server { error?: Error; } - interface IHttpClient { - httpRequest(url: string): Promise; - httpRequest( - options: any, - proxySettings?: IProxySettings - ): Promise; - } + /** @deprecated Kept so existing annotations compile; use the {@link HttpClient} contract. */ + interface IHttpClient extends HttpClient {} interface IRequestResponseData { statusCode: number; @@ -196,79 +192,14 @@ interface IShouldDispose { /** * Describes the type of data sent to analytics service. */ -declare const enum TrackingTypes { - /** - * Defines that the data contains information for initialization of a new Analytics monitor. - */ - Initialization = "initialization", - - /** - * Defines that the data contains exception that should be tracked. - */ - Exception = "exception", - - /** - * Defines that the data contains the answer of the question if user allows to be tracked. - */ - AcceptTrackFeatureUsage = "acceptTrackFeatureUsage", - - /** - * Defines data that will be tracked to Google Analytics. - */ - GoogleAnalyticsData = "googleAnalyticsData", - - /** - * Defines that the broker process should send all the pending information to Analytics. - * After that the process should send information it has finished tracking and die gracefully. - */ - FinishTracking = "FinishTracking", -} /** * Describes the status of the current Analytics status, i.e. has the user allowed to be tracked. */ -declare const enum AnalyticsStatus { - /** - * User has allowed to be tracked. - */ - enabled = "enabled", - - /** - * User has declined to be tracked. - */ - disabled = "disabled", - - /** - * User has not been asked to allow feature and error tracking. - */ - notConfirmed = "not confirmed", -} /** * Describes types of options that manage -- flags. */ -declare const enum OptionType { - /** - * String option - */ - String = "string", - /** - * Boolean option - */ - Boolean = "boolean", - /** - * Number option - */ - Number = "number", - /** - * Array option - */ - Array = "array", - /** - * Object option - */ - Object = "object", -} /** * Describes options that can be passed to fs.readFile method. @@ -285,303 +216,8 @@ interface IReadFileOptions { flag?: string; } -interface IFileSystem { - zipFiles( - zipFile: string, - files: string[], - zipPathCallback: (path: string) => string - ): Promise; - unzip( - zipFile: string, - destinationDir: string, - options?: { overwriteExisitingFiles?: boolean; caseSensitive?: boolean }, - fileFilters?: string[] - ): Promise; - - /** - * Test whether or not the given path exists by checking with the file system. - * @param {string} path Path to be checked. - * @returns {boolean} True if path exists, false otherwise. - */ - exists(path: string): boolean; - - /** - * Deletes a file. - * @param {string} path Path to be deleted. - * @returns {void} undefined - */ - deleteFile(path: string): void; - - /** - * Deletes whole directory. - * @param {string} directory Path to directory that has to be deleted. - * @returns {void} - */ - deleteDirectory(directory: string): void; - - /** - * Deletes whole directory without throwing exceptions. - * @param {string} directory Path to directory that has to be deleted. - * @returns {void} - */ - deleteDirectorySafe(directory: string): void; - - /** - * Returns the size of specified file. - * @param {string} path Path to file. - * @returns {number} File size in bytes. - */ - getFileSize(path: string): number; - - /** - * Returns the size of specified path (recurses into all sub-directories if the path is a directory). - * @param {string} path Path to file or directory. - * @returns {number} File size in bytes. - */ - getSize(path: string): number; - - /** - * Change file timestamps of the file referenced by the supplied path. - * @param {string} path File path - * @param {Date} atime Access time - * @param {Date} mtime Modified time - * @returns {void} - */ - utimes(path: string, atime: Date, mtime: Date): void; - - futureFromEvent( - eventEmitter: NodeJS.EventEmitter, - event: string - ): Promise; - - /** - * Create a new directory and any necessary subdirectories at specified location. - * @param {string} path Directory to be created. - * @returns {void} - */ - createDirectory(path: string): void; - - /** - * Reads contents of directory and returns an array of filenames excluding '.' and '..'. - * @param {string} path Path to directory to be checked. - * @retruns {string[]} Array of filenames excluding '.' and '..' - */ - readDirectory(path: string): string[]; - - /** - * Reads the entire contents of a file. - * @param {string} filename Path to the file that has to be read. - * @param {string} options Options used for reading the file - encoding and flags. - * @returns {string|Buffer} Content of the file as buffer. In case encoding is specified, the content is returned as string. - */ - readFile(filename: string, options?: IReadFileOptions): string | Buffer; - - /** - * Reads the entire contents of a file and returns the result as string. - * @param {string} filename Path to the file that has to be read. - * @param {IReadFileOptions | string} encoding Options used for reading the file - encoding and flags. If options are not passed, utf8 is used. - * @returns {string} Content of the file as string. - */ - readText(filename: string, encoding?: IReadFileOptions | string): string; - - /** - * Reads the entire content of a file and parses it to JSON object. - * @param {string} filename Path to the file that has to be read. - * @param {string} encoding File encoding, defaults to utf8. - * @returns {string} Content of the file as JSON object. - */ - readJson(filename: string, encoding?: string): any; - - readStdin(): Promise; - - /** - * Writes data to a file, replacing the file if it already exists. data can be a string or a buffer. - * @param {string} filename Path to file to be created. - * @param {string | Buffer} data Data to be written to file. - * @param {string} encoding @optional File encoding, defaults to utf8. - * @returns {void} - */ - writeFile(filename: string, data: string | Buffer, encoding?: string): void; - - /** - * Appends data to a file, creating the file if it does not yet exist. Data can be a string or a buffer. - * @param {string} filename Path to file to be created. - * @param {string | Buffer} data Data to be appended to file. - * @param {string} encoding @optional File encoding, defaults to utf8. - * @returns {void} - */ - appendFile(filename: string, data: string | Buffer, encoding?: string): void; - - /** - * Writes JSON data to file. - * @param {string} filename Path to file to be created. - * @param {any} data JSON data to be written to file. - * @param {string} space Identation that will be used for the file. - * @param {string} encoding @optional File encoding, defaults to utf8. - * @returns {void} - */ - writeJson( - filename: string, - data: any, - space?: string, - encoding?: string - ): void; - - /** - * Copies a file. - * @param {string} sourceFileName The original file that has to be copied. - * @param {string} destinationFileName The filepath where the file should be copied. - * @returns {void} - */ - copyFile(sourceFileName: string, destinationFileName: string): void; - - /** - * Returns unique file name based on the passed name by checkin if it exists and adding numbers to the passed name until a non-existent file is found. - * @param {string} baseName The name based on which the unique name will be generated. - * @returns {string} Unique filename. In case baseName does not exist, it will be returned. - */ - getUniqueFileName(baseName: string): string; - - /** - * Checks if specified directory is empty. - * @param {string} directoryPath The directory that will be checked. - * @returns {boolean} True in case the directory is empty. False otherwise. - */ - isEmptyDir(directoryPath: string): boolean; - - isRelativePath( - path: string - ): boolean /* feels so lonely here, I don't have a Future */; - - /** - * Checks if directory exists and if not - creates it. - * @param {string} directoryPath Directory path. - * @returns {void} - */ - ensureDirectoryExists(directoryPath: string): void; - - /** - * Renames file/directory. This method throws error in case the original file name does not exist. - * @param {string} oldPath The original filename. - * @param {string} newPath New filename. - * @returns {string} void. - */ - rename(oldPath: string, newPath: string): void; - - /** - * Renames specified file to the specified name only in case it exists. - * Used to skip ENOENT errors when rename is called directly. - * @param {string} oldPath Path to original file that has to be renamed. If this file does not exists, no operation is executed. - * @param {string} newPath The path where the file will be moved. - * @return {boolean} True in case of successful rename. False in case the file does not exist. - */ - renameIfExists(oldPath: string, newPath: string): boolean; - - /** - * Returns information about the specified file. - * In case the passed path is symlink, the returned information is about the original file. - * @param {string} path Path to file for which the information will be taken. - * @returns {IFsStats} Inforamation about the specified file. - */ - getFsStats(path: string): IFsStats; - - /** - * Returns information about the specified file. - * In case the passed path is symlink, the returned information is about the symlink itself. - * @param {string} path Path to file for which the information will be taken. - * @returns {IFsStats} Inforamation about the specified file. - */ - getLsStats(path: string): IFsStats; - - symlink(sourcePath: string, destinationPath: string, type: "file"): void; - symlink(sourcePath: string, destinationPath: string, type: "dir"): void; - symlink(sourcePath: string, destinationPath: string, type: "junction"): void; - - /** - * Creates a symbolic link. - * Symbolic links are interpreted at run time as if the contents of the - * link had been substituted into the path being followed to find a file - * or directory. - * @param {string} sourcePath The original path of the file/dir. - * @param {string} destinationPath The destination where symlink will be created. - * @param {string} type "file", "dir" or "junction". Default is 'file'. - * Type option is only available on Windows (ignored on other platforms). - * Note that Windows junction points require the destination path to be absolute. - * When using 'junction', the target argument will automatically be normalized to absolute path. - * @returns {void} - */ - symlink(sourcePath: string, destinationPath: string, type?: string): void; - - createReadStream( - path: string, - options?: { - flags?: string; - encoding?: string; - fd?: number; - mode?: number; - bufferSize?: number; - start?: number; - end?: number; - highWaterMark?: number; - } - ): NodeJS.ReadableStream; - createWriteStream( - path: string, - options?: { - flags?: string; - encoding?: string; - string?: string; - } - ): any; - - /** - * Changes file mode of the specified file. In case it is a symlink, the original file's mode is modified. - * @param {string} path Filepath to be modified. - * @param {number | string} mode File mode. - * @returns {void} - */ - chmod(path: string, mode: number | string): void; - - setCurrentUserAsOwner(path: string, owner: string): Promise; - enumerateFilesInDirectorySync( - directoryPath: string, - filterCallback?: (file: string, stat: IFsStats) => boolean, - opts?: { enumerateDirectories?: boolean; includeEmptyDirectories?: boolean } - ): string[]; - - /** - * Hashes a file's contents. - * @param {string} fileName Path to file - * @param {Object} options algorithm and digest encoding. Default values are sha1 for algorithm and hex for encoding - * @return {Promise} The computed shasum - */ - getFileShasum( - fileName: string, - options?: { algorithm?: string; encoding?: "hex" | "base64" } - ): Promise; - - // shell.js wrappers - /** - * @param {string} options Options, can be undefined or a combination of "-r" (recursive) and "-f" (force) - * @param {string[]} files files and direcories to delete - */ - rm(options: string, ...files: string[]): void; - - /** - * Deletes all empty parent directories. - * @param {string} directory The directory from which this method will start looking for empty parents. - * @returns {void} - */ - deleteEmptyParents(directory: string): void; - - /** - * Return the canonicalized absolute pathname. - * NOTE: The method accepts second argument, but it's type and usage is different in Node 4 and Node 6. Once we drop support for Node 4, we can use the second argument as well. - * @param {string} filePath Path to file which should be resolved. - * @returns {string} The canonicalized absolute path to file. - */ - realpath(filePath: string): string; -} +/** @deprecated Kept so existing annotations compile; use the {@link FileSystem} contract. */ +interface IFileSystem extends FileSystem {} // duplicated from fs.Stats, because I cannot import it here interface IFsStats { @@ -611,26 +247,8 @@ interface IOpener { open(filename: string, appname?: string): void; } -interface IErrors { - fail(formatStr: string, ...args: any[]): never; - fail(opts: IFailOptions, ...args: any[]): never; - /** - * @deprecated: use `fail` instead - */ - failWithoutHelp(message: string, ...args: any[]): never; - /** - * @deprecated: use `fail` instead - */ - failWithoutHelp(opts: IFailOptions, ...args: any[]): never; - failWithHelp(formatStr: string, ...args: any[]): never; - failWithHelp(opts: IFailOptions, ...args: any[]): never; - beginCommand( - action: () => Promise, - printCommandHelp: () => Promise - ): Promise; - verifyHeap(message: string): void; - printCallStack: boolean; -} +/** @deprecated Kept so existing annotations compile; use the {@link Errors} contract. */ +interface IErrors extends Errors {} interface IFailOptions { name?: string; @@ -656,23 +274,6 @@ interface ICommandOptions { disableCommandHelpSuggestion?: boolean; } -declare const enum ErrorCodes { - UNCAUGHT = 120, - UNKNOWN = 127, - INVALID_ARGUMENT = 128, - RESOURCE_PROBLEM = 129, - KARMA_FAIL = 130, - UNHANDLED_REJECTION_FAILURE = 131, - DELETED_KILL_FILE = 132, - TESTS_INIT_REQUIRED = 133, - ALL_DEVICES_DISCONNECTED = 134, -} - -interface IFutureDispatcher { - run(): void; - dispatch(action: () => Promise): void; -} - interface ICommandDispatcher { dispatchCommand(): Promise; } @@ -682,66 +283,8 @@ interface ICancellationService extends IDisposable { end(name: string): void; } -interface IQueue { - enqueue(item: T): void; - dequeue(): Promise; -} - -interface IChildProcess extends NodeJS.EventEmitter { - exec( - command: string, - options?: any, - execOptions?: IExecOptions - ): Promise; - execFile(command: string, args: string[]): Promise; - spawn( - command: string, - args?: string[], - options?: any - ): child_process.ChildProcess; // it returns child_process.ChildProcess you can safely cast to it - spawnFromEvent( - command: string, - args: string[], - event: string, - options?: any, - spawnFromEventOptions?: ISpawnFromEventOptions - ): Promise; - trySpawnFromCloseEvent( - command: string, - args: string[], - options?: any, - spawnFromEventOptions?: ISpawnFromEventOptions - ): Promise; - tryExecuteApplication( - command: string, - args: string[], - event: string, - errorMessage: string, - condition?: (childProcess: any) => boolean - ): Promise; - /** - * This is a special case of the child_process.spawn() functionality for spawning Node.js processes. - * In addition to having all the methods in a normal ChildProcess instance, the returned object has a communication channel built-in. - * Note: Unlike the fork() POSIX system call, child_process.fork() does not clone the current process. - * @param {string} modulePath String The module to run in the child - * @param {string[]} args Array List of string arguments You can access them in the child with 'process.argv'. - * @param {string} options Object - * @return {child_process} ChildProcess object. - */ - fork( - modulePath: string, - args?: string[], - options?: { - cwd?: string; - env?: any; - execPath?: string; - execArgv?: string[]; - silent?: boolean; - uid?: number; - gid?: number; - } - ): any; -} +/** @deprecated Kept so existing annotations compile; use the {@link ChildProcess} contract. */ +interface IChildProcess extends ChildProcess {} interface IExecOptions { showStderr: boolean; @@ -781,7 +324,7 @@ interface IAnalyticsService { getStatusMessage( settingName: string, jsonFormat: boolean, - readableSettingName: string + readableSettingName: string, ): Promise; isEnabled(settingName: string): Promise; finishTracking(): Promise; @@ -828,7 +371,7 @@ interface IPrompterOptions extends IAllowEmpty { type IPrompterAnswers = { [id in T]: any }; interface IPrompterQuestion< - T extends IPrompterAnswers = IPrompterAnswers + T extends IPrompterAnswers = IPrompterAnswers, > { type?: string; name?: string; @@ -839,7 +382,7 @@ interface IPrompterQuestion< filter?(input: any, answers: T): any; validate?( input: any, - answers?: T + answers?: T, ): boolean | string | Promise; } @@ -902,15 +445,26 @@ interface IAutoCompletionService { isObsoleteAutoCompletionEnabled(): boolean; } +interface IHookExecutionOptions { + /** + * Set by call sites that fold the returned middlewares around a method (the + * `@hook` decorator). Where nothing consumes them, `ctx.wrap()` rejects + * instead of registering a middleware that would never run. + */ + consumesMiddlewares?: boolean; +} + interface IHooksService { hookArgsName: string; + /** Resolves with the middlewares hooks registered through `ctx.wrap()`. */ executeBeforeHooks( commandName: string, - hookArguments?: IDictionary - ): Promise; + hookArguments?: IDictionary, + options?: IHookExecutionOptions, + ): Promise; executeAfterHooks( commandName: string, - hookArguments?: IDictionary + hookArguments?: IDictionary, ): Promise; } @@ -938,9 +492,7 @@ interface IRejectUnauthorized { * Proxy settings required for http request. */ interface IProxySettings - extends IRejectUnauthorized, - ICredentials, - IProxySettingsBase { + extends IRejectUnauthorized, ICredentials, IProxySettingsBase { /** * Hostname of the machine used for proxy. */ @@ -1001,10 +553,6 @@ interface IProxyService { getInfo(): Promise; } -interface IQrCodeGenerator { - generateDataUri(data: string): Promise; -} - interface IQrCodeImageData { /** * The original URL used for generating QR code image. @@ -1077,7 +625,7 @@ interface ISystemWarning { interface ISysInfo { getSysInfo( - config?: NativeScriptDoctor.ISysInfoConfig + config?: NativeScriptDoctor.ISysInfoConfig, ): Promise; /** * Returns the currently installed version of Xcode. @@ -1137,17 +685,8 @@ interface ISysInfo { getXcodeWarning(): Promise; } -interface IHostInfo { - isWindows: boolean; - isWindows64: boolean; - isWindows32: boolean; - isDarwin: boolean; - isLinux: boolean; - isLinux64: boolean; - dotNetVersion(): Promise; - isDotNet40Installed(message: string): Promise; - getMacOSVersion(): Promise; -} +/** @deprecated Kept so existing annotations compile; use the {@link HostInfo} contract. */ +interface IHostInfo extends HostInfo {} // tslint:disable-next-line:interface-name interface GenericFunction extends Function { @@ -1283,44 +822,8 @@ interface IDashedOption { * Code behind of the "doctor" command * @interface */ -interface IDoctorService { - /** - * Verifies the host OS configuration and prints warnings to the users - * @param configOptions: defines if the result should be tracked by Analytics - * @returns {Promise} - */ - printWarnings(configOptions?: { - trackResult?: boolean; - projectDir?: string; - runtimeVersion?: string; - options?: IOptions; - forceCheck?: boolean; - platform?: string; - }): Promise; - /** - * Runs the setup script on host machine - * @returns {Promise} - */ - runSetupScript(): Promise; - /** - * Checks if the envrironment is properly configured and it is possible to execute local builds - * @returns {Promise} true if the environment is properly configured for local builds - * @param {object} configuration - */ - canExecuteLocalBuild(configuration?: { - platform?: string; - projectDir?: string; - runtimeVersion?: string; - forceCheck?: boolean; - }): Promise; - - /** - * Checks and notifies users for deprecated short imports in their applications. - * @param {string} projectDir Path to the application. - * @returns {void} - */ - checkForDeprecatedShortImportsInAppDir(projectDir: string): void; -} +/** @deprecated Kept so existing annotations compile; use the {@link DoctorService} contract. */ +interface IDoctorService extends DoctorService {} interface IUtils { getParsedTimeout(defaultTimeout: number): number; @@ -1438,14 +941,14 @@ interface IProjectFilesManager { projectFilesPath: string, excludedProjectDirsAndFiles?: string[], filter?: (filePath: string, stat: IFsStats) => boolean, - opts?: any + opts?: any, ): string[]; /** * Checks if the file is excluded */ isFileExcluded( filePath: string, - excludedProjectDirsAndFiles?: string[] + excludedProjectDirsAndFiles?: string[], ): boolean; /** * Returns an object that maps every local file path to device file path @@ -1456,7 +959,7 @@ interface IProjectFilesManager { projectFilesPath: string, files: string[], excludedProjectDirsAndFiles: string[], - projectFilesConfig?: IProjectFilesConfig + projectFilesConfig?: IProjectFilesConfig, ): Promise; /** @@ -1471,7 +974,7 @@ interface IProjectFilesManager { directoryPath: string, platform: string, projectFilesConfig?: IProjectFilesConfig, - excludedDirs?: string[] + excludedDirs?: string[], ): void; } @@ -1487,7 +990,7 @@ interface IProjectFilesProvider { filePath: string, platform: string, projectData: any, - projectFilesConfig?: IProjectFilesConfig + projectFilesConfig?: IProjectFilesConfig, ): string; /** @@ -1500,7 +1003,7 @@ interface IProjectFilesProvider { getProjectFileInfo( filePath: string, platform: string, - projectFilesConfig: IProjectFilesConfig + projectFilesConfig: IProjectFilesConfig, ): IProjectFileInfo; /** * Parses file by removing platform or configuration from its name. @@ -1510,7 +1013,7 @@ interface IProjectFilesProvider { */ getPreparedFilePath( filePath: string, - projectFilesConfig: IProjectFilesConfig + projectFilesConfig: IProjectFilesConfig, ): string; } @@ -1612,7 +1115,7 @@ interface INet { * @returns {boolean} true in case port is in LISTEN state, false otherwise. */ waitForPortToListen( - waitForPortListenData: IWaitForPortListenData + waitForPortListenData: IWaitForPortListenData, ): Promise; } @@ -1621,6 +1124,12 @@ interface IDependencyInformation { version?: string; projectType?: string; excludedPeerDependencies?: string[]; + /** + * Install into "dependencies" instead of "devDependencies". Required for + * packages with native code — the CLI integrates plugin platform files + * (pods, aars) only for regular dependencies. + */ + saveInDependencies?: boolean; } /** @@ -1680,7 +1189,7 @@ interface IiOSNotificationService { awaitNotification( deviceIdentifier: string, socket: number, - timeout: number + timeout: number, ): Promise; /** @@ -1693,7 +1202,7 @@ interface IiOSNotificationService { postNotification( deviceIdentifier: string, notification: string, - commandType?: string + commandType?: string, ): Promise; } diff --git a/lib/common/define-command.ts b/lib/common/define-command.ts new file mode 100644 index 0000000000..c5cf28e5fe --- /dev/null +++ b/lib/common/define-command.ts @@ -0,0 +1,344 @@ +/** + * The declarative command API. Types and pure factories only — this module is + * re-exported from `nativescript/contracts` and must stay side-effect-free, so + * it may not import lib/common/yok (whose import creates global.$injector). + * The runtime bridge onto the legacy registry lives in + * lib/common/services/command-definition-adapter. + */ + +/** + * Symbol.for so that a definition produced by one copy of the CLI is still + * recognised by another — extensions bundle their own node_modules. `unique + * symbol` so the marker can also be spelled in the branded return type. + */ +export const COMMAND_DEFINITION_MARKER: unique symbol = Symbol.for( + "nativescript:cli:commandDefinition", +); + +export type CommandOptionType = "boolean" | "string" | "number" | "array"; + +export interface CommandOptionSpec { + type: CommandOptionType; + /** Value used when the flag is absent from the command line. */ + default?: TValue; + /** Single-dash shorthand, e.g. `-o` for `--output`. */ + alias?: string | string[]; + /** Keeps the value out of analytics and logs. Defaults to false. */ + hasSensitiveValue?: boolean; + /** Reserved for generated help; nothing renders it yet. */ + description?: string; +} + +/** + * A spec whose `default` is required. The required property is what + * `CommandOptionValues` keys off to drop `| undefined` from the value type, so + * it may not be relaxed to an optional one. + */ +export interface DefaultedCommandOptionSpec< + TValue = any, +> extends CommandOptionSpec { + default: TValue; +} + +/** The parts of an option spec a caller supplies; `type` comes from the helper. */ +export type CommandOptionSpecInit = Omit< + CommandOptionSpec, + "type" +>; + +export interface CommandOptionsSchema { + [optionName: string]: CommandOptionSpec; +} + +/** + * An option the command line omitted is absent at runtime, so only a spec that + * declares a `default` yields a value that is always there. + */ +type CommandOptionValue = + TSpec extends CommandOptionSpec + ? TSpec extends { default: any } + ? TValue + : TValue | undefined + : any; + +export type CommandOptionValues = { + [K in keyof TSchema]: CommandOptionValue; +}; + +export interface CommandContext { + /** Positional arguments, after the command name has been consumed. */ + args: string[]; + /** Current value of every option declared in the schema, and nothing else. */ + options: CommandOptionValues; + /** Fails the command with `message` and the usage help suggestion. */ + fail(message: string): never; +} + +export interface CommandDefinition { + /** `"widget|add"`; `|` separates hierarchy levels. Several names alias one command. */ + name: string | string[]; + description?: string; + options?: TSchema; + /** + * `"none"` (the default) rejects positional arguments; `"any"` accepts them. + * Anything finer belongs in `canExecute`, which runs after this policy. + */ + arguments?: "none" | "any"; + canExecute?(context: CommandContext): Promise | boolean; + disableAnalytics?: boolean; + enableHooks?: boolean; + run(context: CommandContext): Promise | void; +} + +/** + * What `defineCommand` returns: a definition carrying the marker in its type, + * so `registerCommandDefinition` can require a definition that went through + * define-time validation rather than any object of the right shape. + */ +export type DefinedCommand = + CommandDefinition & { + readonly [COMMAND_DEFINITION_MARKER]: true; + }; + +interface IOptionHelper { + ( + init: CommandOptionSpecInit & { default: TValue }, + ): DefaultedCommandOptionSpec; + (init?: CommandOptionSpecInit): CommandOptionSpec; +} + +const optionHelper = (type: CommandOptionType): IOptionHelper => + >((init: CommandOptionSpecInit = {}) => ({ + ...init, + type, + })); + +export const booleanOption = optionHelper("boolean"); +export const stringOption = optionHelper("string"); +export const numberOption = optionHelper("number"); +export const arrayOption = optionHelper("array"); + +const DEFINITION_FIELDS = [ + "name", + "description", + "options", + "arguments", + "canExecute", + "disableAnalytics", + "enableHooks", + "run", +]; + +const OPTION_SPEC_FIELDS = [ + "type", + "default", + "alias", + "hasSensitiveValue", + "description", +]; + +const OPTION_TYPES: CommandOptionType[] = [ + "boolean", + "string", + "number", + "array", +]; + +const ACCEPTED_FORM = + 'defineCommand({ name: "widget|add", run(ctx) { ... } }) — with the ' + + "optional fields description, options, arguments, canExecute, " + + "disableAnalytics and enableHooks."; + +const describeDefinition = (definition: any): string => { + const name = definition && definition.name; + if (typeof name === "string" && name.length) { + return `'${name}'`; + } + + if (Array.isArray(name) && typeof name[0] === "string" && name[0].length) { + return `'${name[0]}'`; + } + + return "an unnamed command"; +}; + +const invalid = (definition: any, problem: string): never => { + throw new Error( + `Invalid command definition for ${describeDefinition(definition)}: ` + + `${problem}. Accepted form: ${ACCEPTED_FORM}`, + ); +}; + +const isPlainObject = (value: any): boolean => + !!value && typeof value === "object" && !Array.isArray(value); + +const validateName = (definition: any): void => { + const name = definition.name; + const isUsableName = (value: any) => + typeof value === "string" && value.trim().length > 0; + + if (isUsableName(name)) { + return; + } + + if (Array.isArray(name) && name.length && name.every(isUsableName)) { + return; + } + + invalid( + definition, + "'name' must be a non-empty string, or an array of non-empty strings for a command with aliases", + ); +}; + +const validateOptionSpec = ( + definition: any, + optionName: string, + spec: any, +): void => { + if (!isPlainObject(spec)) { + invalid( + definition, + `option '${optionName}' must be declared with one of booleanOption(), stringOption(), numberOption() or arrayOption()`, + ); + } + + if (OPTION_TYPES.indexOf(spec.type) === -1) { + invalid( + definition, + `option '${optionName}' has type '${spec.type}'; the supported types are ${OPTION_TYPES.join( + ", ", + )} — declare it with one of booleanOption(), stringOption(), numberOption() or arrayOption()`, + ); + } + + const unknownFields = Object.keys(spec).filter( + (field) => OPTION_SPEC_FIELDS.indexOf(field) === -1, + ); + if (unknownFields.length) { + invalid( + definition, + `option '${optionName}' has unknown field(s) ${unknownFields + .map((field) => `'${field}'`) + .join(", ")}; an option spec accepts ${OPTION_SPEC_FIELDS.join(", ")}`, + ); + } + + const aliasIsUsable = + spec.alias === undefined || + typeof spec.alias === "string" || + (Array.isArray(spec.alias) && + spec.alias.length > 0 && + spec.alias.every((entry: any) => typeof entry === "string")); + if (!aliasIsUsable) { + invalid( + definition, + `option '${optionName}' declares an 'alias' that is neither a string nor a non-empty array of strings`, + ); + } + + if ( + spec.hasSensitiveValue !== undefined && + typeof spec.hasSensitiveValue !== "boolean" + ) { + invalid( + definition, + `option '${optionName}' declares a non-boolean 'hasSensitiveValue'`, + ); + } + + if (spec.description !== undefined && typeof spec.description !== "string") { + invalid( + definition, + `option '${optionName}' declares a non-string 'description'`, + ); + } +}; + +const validateDefinition = (definition: any): void => { + if (!isPlainObject(definition)) { + invalid(definition, "expected an object"); + } + + const unknownFields = Object.keys(definition).filter( + (field) => DEFINITION_FIELDS.indexOf(field) === -1, + ); + if (unknownFields.length) { + invalid( + definition, + `unknown field(s) ${unknownFields + .map((field) => `'${field}'`) + .join(", ")}; a definition accepts ${DEFINITION_FIELDS.join(", ")}`, + ); + } + + validateName(definition); + + if (typeof definition.run !== "function") { + invalid(definition, "'run' must be a function"); + } + + if ( + definition.arguments !== undefined && + definition.arguments !== "none" && + definition.arguments !== "any" + ) { + invalid( + definition, + `'arguments' is '${definition.arguments}'; it must be "none" or "any"`, + ); + } + + if ( + definition.canExecute !== undefined && + typeof definition.canExecute !== "function" + ) { + invalid(definition, "'canExecute' must be a function"); + } + + for (const flag of ["disableAnalytics", "enableHooks"]) { + if ( + definition[flag] !== undefined && + typeof definition[flag] !== "boolean" + ) { + invalid(definition, `'${flag}' must be a boolean`); + } + } + + if (definition.description !== undefined) { + if (typeof definition.description !== "string") { + invalid(definition, "'description' must be a string"); + } + } + + if (definition.options !== undefined) { + if (!isPlainObject(definition.options)) { + invalid( + definition, + "'options' must be an object keyed by the long option name", + ); + } + + for (const optionName of Object.keys(definition.options)) { + validateOptionSpec( + definition, + optionName, + definition.options[optionName], + ); + } + } +}; + +export function defineCommand( + definition: CommandDefinition, +): DefinedCommand { + validateDefinition(definition); + + const marked: any = { ...definition }; + marked[COMMAND_DEFINITION_MARKER] = true; + return marked; +} + +export function isCommandDefinition(value: any): value is DefinedCommand { + return !!value && (value)[COMMAND_DEFINITION_MARKER] === true; +} diff --git a/lib/common/define-hook.ts b/lib/common/define-hook.ts new file mode 100644 index 0000000000..c4f0b8e03f --- /dev/null +++ b/lib/common/define-hook.ts @@ -0,0 +1,257 @@ +/** + * The typed hook-authoring API. Kept import-free so that a hook (or an + * extension carrying its own copy of the CLI) can load it without booting a + * second runtime — importing lib/common/yok creates global.$injector. + */ + +/** + * `Symbol.for` rather than a module-local symbol: an extension may resolve a + * duplicated copy of the CLI from its own node_modules, and the running CLI + * still has to recognize definitions minted by that copy. + * + * Assigned as a plain enumerable property so that `{ ...definition }` keeps the + * marker; symbols stay invisible to Object.keys/for..in/JSON either way. + */ +export const HOOK_DEFINITION_MARKER = Symbol.for( + "nativescript:cli:hookDefinition", +); + +/** + * Wraps the method the hook point decorates. `next` continues the chain — call + * it with `args` to run the original, or skip it to short-circuit. + */ +export type HookMiddleware = ( + args: any[], + next: (...args: any[]) => any, +) => any; + +export interface HookContext { + /** + * The payload of the operation being hooked. Its shape depends on the hook + * point, and it is the caller's own object: mutating it is a supported + * channel for influencing the operation. Hook points fired by command + * dispatch carry no payload at all, hence `undefined`. + */ + payload: TPayload | undefined; + + /** + * Registers a middleware around the method this hook point decorates. + * Available only to before-hooks of the hook points that fold middlewares + * around a method; elsewhere it throws rather than dropping the middleware. + */ + wrap(middleware: HookMiddleware): void; + + /** + * Ends the handler and fails the command with `message`. + * + * Typed `never` because it stops the handler by throwing, so nothing after + * the call runs. + */ + fail(message: string): never; + + /** + * Ends the handler and logs `message` as a warning; the command continues. + * + * Typed `never` because it stops the handler by throwing, so nothing after + * the call runs — only the command outlives it. + */ + skip(message: string): never; +} + +export type HookHandler = ( + ctx: HookContext, +) => void | Promise; + +/** The object bag accepted by `defineHook`. */ +export interface HookDefinitionInput { + /** Hook point, in the hyphen convention: `before-prepare`, `after-watch`. */ + name: string; + run: HookHandler; +} + +export interface HookDefinition { + /** Hook point, in the hyphen convention: `before-prepare`, `after-watch`. */ + readonly name: string; + readonly run: HookHandler; +} + +export interface HookInvocation { + context: HookContext; + /** Populated by `ctx.wrap()` while the handler runs. */ + middlewares: HookMiddleware[]; +} + +const DEFINITION_FIELDS = ["name", "run"]; + +const ACCEPTED_FORMS = + 'defineHook({ name: "before-prepare", run: (ctx) => {} }) or ' + + 'defineHook("before-prepare", (ctx) => {})'; + +function describeDefinition(name: any): string { + return typeof name === "string" && name.length + ? JSON.stringify(name) + : ""; +} + +function failToDefine(message: string): never { + throw new Error(`${message} Accepted forms: ${ACCEPTED_FORMS}.`); +} + +export function defineHook( + definition: HookDefinitionInput, +): HookDefinition; +export function defineHook( + name: string, + run: HookHandler, +): HookDefinition; +export function defineHook( + nameOrDefinition: string | HookDefinitionInput, + run?: HookHandler, +): HookDefinition { + const input = normalizeDefinitionInput(nameOrDefinition, run); + const definition: any = { name: input.name, run: input.run }; + definition[HOOK_DEFINITION_MARKER] = true; + + return definition; +} + +function normalizeDefinitionInput( + nameOrDefinition: string | HookDefinitionInput, + run?: HookHandler, +): HookDefinitionInput { + if (typeof nameOrDefinition === "string") { + if (!nameOrDefinition.length) { + failToDefine("defineHook() requires a non-empty hook point name."); + } + + if (typeof run !== "function") { + failToDefine( + `defineHook(${describeDefinition(nameOrDefinition)}) requires a handler function as its second argument.`, + ); + } + + return { name: nameOrDefinition, run }; + } + + if ( + !nameOrDefinition || + typeof nameOrDefinition !== "object" || + Array.isArray(nameOrDefinition) + ) { + failToDefine("defineHook() was called with an unsupported argument."); + } + + const unknownFields = Object.keys(nameOrDefinition).filter( + (field) => DEFINITION_FIELDS.indexOf(field) === -1, + ); + if (unknownFields.length) { + failToDefine( + `defineHook(${describeDefinition(nameOrDefinition.name)}) received unknown ` + + `field${unknownFields.length > 1 ? "s" : ""} ` + + `${unknownFields.map((field) => JSON.stringify(field)).join(", ")}. ` + + `Supported fields: ${DEFINITION_FIELDS.map((field) => JSON.stringify(field)).join(", ")}.`, + ); + } + + if (typeof nameOrDefinition.name !== "string" || !nameOrDefinition.name) { + failToDefine( + 'defineHook() requires a non-empty "name" naming the hook point.', + ); + } + + if (typeof nameOrDefinition.run !== "function") { + failToDefine( + `defineHook(${describeDefinition(nameOrDefinition.name)}) requires "run" to be a function.`, + ); + } + + return { name: nameOrDefinition.name, run: nameOrDefinition.run }; +} + +export function isHookDefinition( + value: any, +): value is HookDefinition { + return ( + !!value && + (typeof value === "object" || typeof value === "function") && + value[HOOK_DEFINITION_MARKER] === true && + typeof value.run === "function" && + typeof value.name === "string" + ); +} + +export interface HookInvocationOptions { + /** The hook point the definition runs at; used in diagnostics. */ + hookName: string; + /** + * Whether the caller folds the collected middlewares around a method. Only + * the `@hook`-decorated before-points do; everywhere else `ctx.wrap()` has + * nothing to wrap and says so instead of silently dropping the middleware. + */ + consumesMiddlewares?: boolean; +} + +/** + * Derives the context from the raw hook argument bag: the `hookArgs` wrapper + * when the hook point supplies one, the bag itself for hook points that pass + * their keys at the top level, and nothing when there is no payload. + */ +export function createHookInvocation( + hookArguments: any, + options: HookInvocationOptions, +): HookInvocation { + const { hookName, consumesMiddlewares } = options; + const middlewares: HookMiddleware[] = []; + const context: HookContext = { + payload: derivePayload(hookArguments), + wrap(middleware: HookMiddleware): void { + if (!consumesMiddlewares) { + throw new Error( + `ctx.wrap() is not available at the "${hookName}" hook point: nothing folds the middleware around a method there, so it would never run.`, + ); + } + + if (typeof middleware !== "function") { + throw new Error( + `ctx.wrap() expects a function at the "${hookName}" hook point.`, + ); + } + + middlewares.push(middleware); + }, + fail(message: string): never { + throw new Error(hookMessage(message, hookName, "fail")); + }, + skip(message: string): never { + const error: any = new Error(hookMessage(message, hookName, "skip")); + // The pair the hooks service checks for to downgrade a rejection. + error.stopExecution = false; + error.errorAsWarning = true; + throw error; + }, + }; + + return { context, middlewares }; +} + +function hookMessage( + message: string, + hookName: string, + method: string, +): string { + return typeof message === "string" && message.trim().length + ? message + : `The "${hookName}" hook called ctx.${method}() without a message.`; +} + +function derivePayload(hookArguments: any): any { + if (!hookArguments || typeof hookArguments !== "object") { + return undefined; + } + + if ("hookArgs" in hookArguments) { + return hookArguments["hookArgs"]; + } + + return Object.keys(hookArguments).length ? hookArguments : undefined; +} diff --git a/lib/common/definitions/commands.d.ts b/lib/common/definitions/commands.d.ts index 32511970f4..b2e43470e6 100644 --- a/lib/common/definitions/commands.d.ts +++ b/lib/common/definitions/commands.d.ts @@ -16,6 +16,13 @@ interface ICommand extends ICommandOptions { dashedOptions?: IDictionary; isHierarchicalCommand?: boolean; + /** + * Set on commands that forward their options to another CLI: the options + * they accept are not knowable from this CLI's option dictionary, so + * validating them here would reject the other CLI's flags. + */ + skipOptionsValidation?: boolean; + /** * Describes the action that will be executed after the command succeeds. * @param {string[]} args Arguments passed to the command. diff --git a/lib/common/definitions/extensibility.d.ts b/lib/common/definitions/extensibility.d.ts index fbfd3be6d5..dceac2005c 100644 --- a/lib/common/definitions/extensibility.d.ts +++ b/lib/common/definitions/extensibility.d.ts @@ -30,6 +30,13 @@ interface IExtensionData extends IExtensionName { * Full path to the directory of the installed extension. */ pathToExtension: string; + + /** + * Names of the commands the extension contributes, as declared in the commands key of the nativescript key of its package.json. + * The key may be a map of command name to the module implementing it, in which case these are its keys, or the legacy array of command names, in which case these are its entries. + * The property is not set when the extension declares no commands. + */ + commands?: string[]; } /** diff --git a/lib/common/definitions/key-commands.d.ts b/lib/common/definitions/key-commands.ts similarity index 90% rename from lib/common/definitions/key-commands.d.ts rename to lib/common/definitions/key-commands.ts index 2c4522b929..ff08673439 100644 --- a/lib/common/definitions/key-commands.d.ts +++ b/lib/common/definitions/key-commands.ts @@ -29,7 +29,7 @@ export type IKeysLowerCase = export type IKeysUpperCase = Uppercase; -export const enum SpecialKeys { +export enum SpecialKeys { CtrlC = "\u0003", QuestionMark = "?", } @@ -41,11 +41,11 @@ export type IValidKeyName = IKeysLowerCase | IKeysUpperCase | IKeysSpecial; export interface IKeyCommandHelper { attachKeyCommands: ( platform: IKeyCommandPlatform, - processType: SupportedProcessType + processType: SupportedProcessType, ) => void; - addOverride(key: IValidKeyName, execute: () => Promise); - removeOverride(key: IValidKeyName); + addOverride(key: IValidKeyName, execute: () => Promise): void; + removeOverride(key: IValidKeyName): void; printCommands(platform: IKeyCommandPlatform): void; } diff --git a/lib/common/definitions/logger.d.ts b/lib/common/definitions/logger.d.ts index d0f6d7835c..48089eb558 100644 --- a/lib/common/definitions/logger.d.ts +++ b/lib/common/definitions/logger.d.ts @@ -2,6 +2,7 @@ import { Layout, LoggingEvent, Configuration, Level } from "log4js"; import { EventEmitter } from "events"; import { LoggerLevel } from "../../constants"; import { IDictionary } from "../declarations"; +import type { Logger } from "../../contracts/logger"; declare global { interface IAppenderOptions extends IDictionary { @@ -14,28 +15,14 @@ declare global { appenderOptions?: IAppenderOptions; } - interface ILogger { - initialize(opts?: ILoggerOptions): void; - initializeCliLogger(opts?: ILoggerOptions): void; - getLevel(): string; - fatal(formatStr?: any, ...args: any[]): void; - error(formatStr?: any, ...args: any[]): void; - warn(formatStr?: any, ...args: any[]): void; - info(formatStr?: any, ...args: any[]): void; - debug(formatStr?: any, ...args: any[]): void; - trace(formatStr?: any, ...args: any[]): void; - printMarkdown(...args: any[]): void; - prepare(item: any): string; - isVerbose(): boolean; - clearScreen(): void; - } + /** @deprecated Kept so existing annotations compile; use the {@link Logger} contract. */ + interface ILogger extends Logger {} interface Log4JSAppenderConfiguration extends Configuration { layout: Layout; } - interface Log4JSEmitAppenderConfiguration - extends Log4JSAppenderConfiguration { + interface Log4JSEmitAppenderConfiguration extends Log4JSAppenderConfiguration { emitter: EventEmitter; } } diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 0d4b58638b..bb522079fa 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -11,6 +11,7 @@ import { IHasEmulatorOption, IDisposable, } from "../declarations"; +import type { DevicesService } from "../../contracts/devices-service"; declare global { export module Mobile { @@ -258,8 +259,7 @@ declare global { * Describes different options for filtering device logs. */ interface IDeviceLogOptions - extends IDictionary, - Partial { + extends IDictionary, Partial { /** * Process id of the application on the device. */ @@ -284,8 +284,7 @@ declare global { * Describes required methods for getting iOS Simulator's logs. */ interface IiOSSimulatorLogProvider - extends NodeJS.EventEmitter, - IShouldDispose { + extends NodeJS.EventEmitter, IShouldDispose { /** * Starts the process for getting simulator logs and emits and DEVICE_LOG_EVENT_NAME event. * @param {string} deviceId The unique identifier of the device. @@ -429,6 +428,16 @@ declare global { interface IDeviceFileSystem { listFiles(devicePath: string, appIdentifier?: string): Promise; + /** + * Returns the entries of a directory inside the application's + * sandbox, or null when the directory cannot be read. Currently + * implemented only for physical iOS devices (AFC), where it backs + * the post-transfer livesync verification. + */ + getDirectoryEntries?( + devicePath: string, + appIdentifier: string, + ): Promise; getFile( deviceFilePath: string, appIdentifier: string, @@ -523,7 +532,7 @@ declare global { } interface IiOSSimulatorDiscovery extends IDeviceDiscovery { - checkForAvailableSimulators(): Promise; + checkForAvailableSimulators(): Promise; } interface IAndroidDeviceDiscovery extends IDeviceDiscovery { @@ -533,8 +542,7 @@ declare global { /** * Describes options that can be passed to devices service's initialization method. */ - interface IDevicesServicesInitializationOptions - extends Partial { + interface IDevicesServicesInitializationOptions extends Partial { /** * If passed will start an emulator if necesasry. */ @@ -580,107 +588,8 @@ declare global { [platform: string]: string; } - interface IDevicesService extends NodeJS.EventEmitter, IPlatform { - hasDevices: boolean; - deviceCount: number; - - execute( - action: (device: Mobile.IDevice) => Promise, - canExecute?: (dev: Mobile.IDevice) => boolean, - options?: { allowNoDevices?: boolean }, - ): Promise[]>; - - /** - * Initializes DevicesService, so after that device operations could be executed. - * @param {IDevicesServicesInitializationOptions} data Defines the options which will be used for whole devicesService. - * @return {Promise} - */ - initialize(data?: IDevicesServicesInitializationOptions): Promise; - - /** - * Add an IDeviceDiscovery instance which will from now on report devices. The instance should implement IDeviceDiscovery and raise "deviceFound" and "deviceLost" events. - * @param {IDeviceDiscovery} deviceDiscovery Instance, implementing IDeviceDiscovery and raising raise "deviceFound" and "deviceLost" events. - * @return {void} - */ - addDeviceDiscovery(deviceDiscovery: IDeviceDiscovery): void; - getDevices(): Mobile.IDeviceInfo[]; - - /** - * Gets device instance by specified identifier or number. - * @param {string} deviceOption The specified device identifier or number. - * @returns {Promise} Instance of IDevice. - */ - getDevice(deviceOption: string): Promise; - getDevicesForPlatform(platform: string): Mobile.IDevice[]; - getDeviceInstances(): Mobile.IDevice[]; - getDeviceByDeviceOption(): Mobile.IDevice; - isAndroidDevice(device: Mobile.IDevice): boolean; - isiOSDevice(device: Mobile.IDevice): boolean; - isiOSSimulator(device: Mobile.IDevice): boolean; - isOnlyiOSSimultorRunning(): boolean; - isAppInstalledOnDevices( - deviceIdentifiers: string[], - appIdentifier: string, - framework: string, - projectDir: string, - ): Promise[]; - setLogLevel(logLevel: string, deviceIdentifier?: string): void; - deployOnDevices( - deviceIdentifiers: string[], - packageFile: string, - packageName: string, - framework: string, - projectDir: string, - ): Promise[]; - getDeviceByIdentifier(identifier: string): Mobile.IDevice; - mapAbstractToTcpPort( - deviceIdentifier: string, - appIdentifier: string, - framework: string, - ): Promise; - getDebuggableApps( - deviceIdentifiers: string[], - ): Promise[]; - getDebuggableViews( - deviceIdentifier: string, - appIdentifier: string, - ): Promise; - - /** - * Returns all applications installed on the specified device. - * @param {string} deviceIdentifer The identifier of the device for which to get installed applications. - * @returns {Promise} Array of all application identifiers of the apps installed on device. - */ - getInstalledApplications(deviceIdentifier: string): Promise; - - /** - * Returns all available iOS and/or Android emulators. - * @param options The options that can be passed to filter the result. - * @returns {Promise} Dictionary with the following format: { ios: { devices: Mobile.IDeviceInfo[], errors: string[] }, android: { devices: Mobile.IDeviceInfo[], errors: string[]}}. - */ - getEmulatorImages( - options?: Mobile.IListEmulatorsOptions, - ): Promise; - - /** - * Starts an emulator by provided options. - * @param options - * @returns {Promise} - Returns array of errors. - */ - startEmulator(options?: IStartEmulatorOptions): Promise; - - /** - * Returns a single device based on the specified options. If more than one devices are matching, - * prompts the user for a manual choice or returns the first one for non interactive terminals. - */ - pickSingleDevice( - options: IPickSingleDeviceOptions, - ): Promise; - - getPlatformsFromDeviceDescriptors( - deviceDescriptors: ILiveSyncDeviceDescriptor[], - ): string[]; - } + /** @deprecated Kept so existing annotations compile; use the {@link DevicesService} contract. */ + interface IDevicesService extends DevicesService {} interface IPickSingleDeviceOptions { /** @@ -1261,8 +1170,7 @@ declare global { } interface IDeviceLookingOptions - extends IHasEmulatorOption, - IHasDetectionInterval { + extends IHasEmulatorOption, IHasDetectionInterval { shouldReturnImmediateResult: boolean; platform: string; fullDiscovery?: boolean; @@ -1388,8 +1296,7 @@ declare global { /** * Describes information about application on device. */ - interface IDeviceApplicationInformation - extends IDeviceApplicationInformationBase { + interface IDeviceApplicationInformation extends IDeviceApplicationInformationBase { /** * The framework of the project (Cordova or NativeScript). */ diff --git a/lib/common/definitions/yok.d.ts b/lib/common/definitions/yok.d.ts index 08c663bec5..89c544444b 100644 --- a/lib/common/definitions/yok.d.ts +++ b/lib/common/definitions/yok.d.ts @@ -1,56 +1,64 @@ -import { IDisposable, IDictionary } from "../declarations"; -import { ICommand } from "./commands"; -import { IKeyCommand, IValidKeyName } from "./key-commands"; +import { IDictionary } from "../declarations"; +import { Injector } from "../di/injector"; +import { Provider } from "../di/providers"; +import { CommandRegistry } from "../contracts/command-registry"; +import { KeyCommandRegistry } from "../contracts/key-command-registry"; +import { ModuleRegistry } from "../contracts/module-registry"; +import { PublicApiBuilder } from "../contracts/public-api-builder"; -interface IInjector extends IDisposable { - require(name: string, file: string): void; - require(names: string[], file: string): void; - requirePublic(names: string | string[], file: string): void; - requirePublicClass(names: string | string[], file: string): void; - requireCommand(name: string, file: string): void; - requireCommand(names: string[], file: string): void; - requireKeyCommand(name: IValidKeyName, file: string): void; +/** + * The legacy injector facade surface. It extends the token-based `Injector` — + * the facade IS an injector — and adds the legacy subsystems, whose members + * are individually @deprecated. Only the `Yok` class hierarchy implements + * this; the interface survives until the hook/extension deprecation completes. + */ +interface IInjector + extends + Injector, + CommandRegistry, + KeyCommandRegistry, + ModuleRegistry, + PublicApiBuilder { /** * Resolves an implementation by constructor function. * The injector will create new instances for every call. + * @deprecated Use Injector.createInstance. */ resolve(ctor: Function, ctorArguments?: { [key: string]: any }): any; + /** + * @deprecated Use Injector.createInstance. + */ resolve(ctor: Function, ctorArguments?: { [key: string]: any }): T; /** * Resolves an implementation by name. * The injector will create only one instance per name and return the same instance on subsequent calls. + * @deprecated Use inject(Token) in an injection context, or Injector.get. */ resolve(name: string, ctorArguments?: IDictionary): any; + /** + * @deprecated Use inject(Token) in an injection context, or Injector.get. + */ resolve(name: string, ctorArguments?: IDictionary): T; - resolveCommand(name: string): ICommand; - resolveKeyCommand(key: string): IKeyCommand; + /** + * @deprecated Legacy name-based registration. Use the Provider overload or + * provide(); a contract's token name keeps string spellings resolvable. + */ register(name: string, resolver: any, shared?: boolean): void; - registerCommand(name: string, resolver: any): void; - registerCommand(names: string[], resolver: any): void; - registerKeyCommand(key: IValidKeyName, resolver: any): void; - getRegisteredCommandsNames(includeDev: boolean): string[]; - getRegisteredKeyCommandsNames(): string[]; + register(providers: Provider | Provider[]): void; + /** + * @deprecated String-reflective help templating; removable only together + * with the help-template pipeline. + */ dynamicCallRegex: RegExp; - dynamicCall(call: string, args?: any[]): Promise; - isDefaultCommand(commandName: string): boolean; - isValidHierarchicalCommand( - commandName: string, - commandArguments: string[] - ): Promise; - getChildrenCommandsNames(commandName: string): string[]; - buildHierarchicalCommand( - parentCommandName: string, - commandLineArguments: string[] - ): any; - publicApi: any; - /** - * Defines if it's allowed to override already required module. - * This can be used in order to allow redefinition of modules, for example $logger can be replaced by a plugin. - * Default value is false. + * @deprecated See dynamicCallRegex. */ - overrideAlreadyRequiredModule: boolean; + dynamicCall(call: string, args?: any[]): Promise; } +/** + * @deprecated The process-wide legacy injector global. New code receives the + * container via inject(Injector) from lib/common/di. + */ declare var $injector: IInjector; diff --git a/lib/common/deprecation.ts b/lib/common/deprecation.ts new file mode 100644 index 0000000000..5913b18d97 --- /dev/null +++ b/lib/common/deprecation.ts @@ -0,0 +1,95 @@ +/** + * Central reporting point for invocations of legacy/deprecated CLI APIs. + * + * The severity is a single dial so the same call sites can be staged over + * releases: trace (observe usage) → warn (tell users) → error (removal). + * `NS_DEPRECATIONS=warn|error` previews a stricter stage ahead of the default. + */ + +type DeprecationStage = "trace" | "warn" | "error"; + +const DEFAULT_STAGE: DeprecationStage = "trace"; + +interface IDeprecationLogger { + trace(...args: any[]): void; + warn(...args: any[]): void; +} + +export interface IDeprecationReport { + /** Stable identifier of the deprecated API, e.g. "hooks.param-name-signature". */ + api: string; + /** Distinguishes call sites of one API, e.g. a hook path or extension name. */ + detail?: string; + /** + * Logger to report through. When omitted, the logger is resolved lazily from + * the global injector at call time — never at import time — and the report + * is dropped if no logger is resolvable yet. + */ + logger?: IDeprecationLogger; +} + +const reported = new Set(); + +export function reportDeprecation(report: IDeprecationReport): void { + const stage = getDeprecationStage(); + const message = formatMessage(report); + + if (stage === "error") { + throw new Error(message); + } + + const key = report.detail ? `${report.api}::${report.detail}` : report.api; + if (reported.has(key)) { + return; + } + + const logger = report.logger || tryResolveGlobalLogger(); + if (!logger) { + // Deliberately not latched: a report dropped for lack of a logger must + // still be deliverable once one becomes resolvable. + return; + } + reported.add(key); + + if (stage === "warn") { + logger.warn(message); + } else { + logger.trace(message); + } +} + +/** Test seam: reports are deduplicated once per process otherwise. */ +export function clearReportedDeprecations(): void { + reported.clear(); +} + +function formatMessage(report: IDeprecationReport): string { + const detail = report.detail ? ` (${report.detail})` : ""; + return ( + `Legacy CLI API used: ${report.api}${detail}. ` + + `This API is planned for deprecation in a future release; ` + + `set NS_DEPRECATIONS=warn or NS_DEPRECATIONS=error to preview stricter handling.` + ); +} + +function getDeprecationStage(): DeprecationStage { + const value = (process.env.NS_DEPRECATIONS || "").toLowerCase(); + if (value === "warn" || value === "error" || value === "trace") { + return value; + } + return DEFAULT_STAGE; +} + +function tryResolveGlobalLogger(): IDeprecationLogger | null { + try { + // Required at call time: yok imports this module, so a static import + // would be a cycle. Every reporting site already runs with yok loaded. + const injector = require("./yok").getInjector(); + if (!injector) { + return null; + } + return injector.resolve("logger"); + } catch (err) { + return null; + } +} diff --git a/lib/common/di/contract.ts b/lib/common/di/contract.ts new file mode 100644 index 0000000000..cc03be9f02 --- /dev/null +++ b/lib/common/di/contract.ts @@ -0,0 +1,88 @@ +/** + * The name is stored under a `Symbol.for` key deliberately: extensions install + * into their own node_modules tree, so a duplicated copy of this module (and + * of any contract class) must write and read the same property key. A unique + * `Symbol()` would make duplicate copies mutually invisible and break the + * name-fallback lookup in `Injector.get()`. + */ +export const CONTRACT_NAME = Symbol.for("nativescript:di:contractName"); + +export interface IContractOptions { + /** + * Canonical token name, without the `$` prefix. Must be an explicit string + * literal — never derive it from `class.name`, which changes under + * minification. + */ + name: string; +} + +// Per module instance on purpose: a duplicated CLI copy in an extensions tree +// carries its own registry, so contracts redeclared by another copy never +// false-positive here. +const mintedNames = new Map(); + +function describeOwner(owner: object): string { + if (typeof owner === "function") { + return `contract '${owner.name || ""}'`; + } + return "an injection token"; +} + +/** + * Claims a token name for `owner`. Both `@Contract` and `InjectionToken` mint + * here so the two kinds share one namespace: a contract and a token that claim + * the same name would be two tokens silently aliasing one registration. + */ +export function mintTokenName(name: string, owner: object): void { + const existing = mintedNames.get(name); + if (existing && existing !== owner) { + throw new Error( + `Token name '${name}' is already used by ${describeOwner(existing)}. ` + + `Token names must be unique — a duplicate silently aliases two tokens.`, + ); + } + mintedNames.set(name, owner); +} + +/** + * Marks an abstract class as a DI token. The decorated class resolves by + * object identity first and by its name on a miss, so duplicated copies of a + * contract remain interchangeable across node_modules trees. + */ +export function Contract( + options: IContractOptions, +): (target: Function) => void { + const { name } = options; + return (target: Function): void => { + mintTokenName(name, target); + Object.defineProperty(target, CONTRACT_NAME, { + value: name, + writable: false, + enumerable: false, + configurable: false, + }); + }; +} + +/** + * Reads the decorator-set name. Own-property check only: an implementation + * class extending a contract inherits the property, but must not itself act + * as a token. + */ +export function getContractName(token: any): string | undefined { + if ( + typeof token === "function" && + Object.prototype.hasOwnProperty.call(token, CONTRACT_NAME) + ) { + return (token)[CONTRACT_NAME]; + } + return undefined; +} + +/** + * Test seam — the duplicate-name registry (contracts and injection tokens + * alike) otherwise persists per process. + */ +export function clearMintedContractNames(): void { + mintedNames.clear(); +} diff --git a/lib/common/di/forward-ref.ts b/lib/common/di/forward-ref.ts new file mode 100644 index 0000000000..882559d0d3 --- /dev/null +++ b/lib/common/di/forward-ref.ts @@ -0,0 +1,25 @@ +// `Symbol.for` so a duplicated CLI copy in an extensions tree marks thunks +// with the same key this copy reads — mirrors the CONTRACT_NAME reasoning. +const FORWARD_REF = Symbol.for("nativescript:di:forwardRef"); + +/** + * Defers a token reference until the container reads it — for provider arrays + * evaluated at module load, where a class declared later in the file (TDZ) or + * reached through a circular import is not yet a usable binding. Same + * semantics as Angular's forwardRef; resolved at registration and lookup. + * + * This defers *references*, not construction: it cannot break an + * instantiation cycle between two services. For that, inject the Injector and + * resolve late. + */ +export function forwardRef(fn: () => T): T { + (fn)[FORWARD_REF] = true; + return fn; +} + +export function resolveForwardRef(token: T): T { + if (typeof token === "function" && (token)[FORWARD_REF] === true) { + return (token)(); + } + return token; +} diff --git a/lib/common/di/index.ts b/lib/common/di/index.ts new file mode 100644 index 0000000000..8a3ca29b4f --- /dev/null +++ b/lib/common/di/index.ts @@ -0,0 +1,30 @@ +export { Injector } from "./injector"; +export type { InjectOptions } from "./injector"; +export { inject, runInInjectionContext } from "./inject"; +export { forwardRef, resolveForwardRef } from "./forward-ref"; +export { + Contract, + getContractName, + CONTRACT_NAME, + clearMintedContractNames, +} from "./contract"; +export type { IContractOptions } from "./contract"; +export { + InjectionToken, + getInjectionTokenName, + INJECTION_TOKEN_NAME, +} from "./injection-token"; +export { provide, provideLazy } from "./providers"; +export type { + Provider, + InternalProvider, + ProviderToken, + Type, + AbstractType, + IClassProvider, + IValueProvider, + IFactoryProvider, + ILazyClassProvider, + ILegacyClassProvider, + ILazyRequireProvider, +} from "./providers"; diff --git a/lib/common/di/inject.ts b/lib/common/di/inject.ts new file mode 100644 index 0000000000..c29be1bed2 --- /dev/null +++ b/lib/common/di/inject.ts @@ -0,0 +1,77 @@ +import type { Injector, InjectOptions } from "./injector"; +import type { ProviderToken } from "./providers"; + +/** + * The injection context lives on globalThis under a `Symbol.for` key rather + * than in a module-local variable: a hook or extension module can resolve a + * DIFFERENT copy of this file than the one the running CLI set the context + * through (a nested nativescript install, or a project-local copy under a + * globally-run CLI), and a module-local slot would make that copy's inject() + * throw despite being synchronously inside a valid context. + */ +const CONTEXT_SLOT = Symbol.for("nativescript:di:injectionContext"); + +interface IInjectionContextFrame { + injector: Injector; + /** Identifies which loaded copy of this module set the frame. */ + owner: object; +} + +// One per loaded copy of this module — the cross-copy detection marker. +const COPY_ID = {}; + +let reportedCrossCopyUse = false; + +function currentFrame(): IInjectionContextFrame | null { + return (globalThis)[CONTEXT_SLOT] || null; +} + +export function inject(token: ProviderToken): T; +export function inject( + token: ProviderToken, + options: InjectOptions & { optional: true }, +): T | null; +export function inject( + token: ProviderToken, + options: InjectOptions, +): T; +export function inject( + token: ProviderToken, + options?: InjectOptions, +): T | null { + const frame = currentFrame(); + if (!frame) { + throw new Error( + "inject() can only be called from an injection context — a field " + + "initializer, a constructor, or a provider factory running under " + + "runInInjectionContext(). It is not valid after an await; inject " + + "the Injector itself and use injector.get() for late lookups.", + ); + } + + if (frame.owner !== COPY_ID && !reportedCrossCopyUse) { + reportedCrossCopyUse = true; + const logger = frame.injector.get("logger", { optional: true }); + if (logger) { + logger.warn( + `A second copy of the NativeScript CLI (${__dirname}) is serving ` + + `inject() in this process. This works, but loads the CLI twice; ` + + `extensions and projects should declare nativescript as a ` + + `peerDependency so the running copy is shared.`, + ); + } + } + + return frame.injector.get(token, options); +} + +export function runInInjectionContext(injector: Injector, fn: () => T): T { + const g = globalThis; + const previous = g[CONTEXT_SLOT]; + g[CONTEXT_SLOT] = { injector, owner: COPY_ID }; + try { + return fn(); + } finally { + g[CONTEXT_SLOT] = previous; + } +} diff --git a/lib/common/di/injection-token.ts b/lib/common/di/injection-token.ts new file mode 100644 index 0000000000..fa812d7564 --- /dev/null +++ b/lib/common/di/injection-token.ts @@ -0,0 +1,72 @@ +import { mintTokenName } from "./contract"; + +/** + * Mirrors CONTRACT_NAME's `Symbol.for` reasoning: an extension's duplicated + * copy of this module must read the marker off tokens minted by the running + * copy, which a unique `Symbol()` would hide. + */ +export const INJECTION_TOKEN_NAME = Symbol.for( + "nativescript:di:injectionTokenName", +); + +/** + * A typed DI token for a dependency that is not a class — an imported module + * namespace, a plain value, a function. `@Contract` covers services, which + * have an abstract class to decorate; this covers everything else. + * + * The description doubles as the legacy registry name, exactly as a contract's + * name does, so a token is a typed alias over the registration it names: + * + * ```ts + * const XCODE = new InjectionToken( + * "xcode", + * ); + * inject(XCODE); // finds register("xcode", …) untouched + * ``` + */ +export class InjectionToken { + /** + * Phantom, never assigned: with no member mentioning `T` the type parameter + * is erased and every token becomes assignable to every other one. + */ + declare private readonly resolvedType: T; + + /** + * @param description Canonical registry name. A leading `$` is stripped, so + * the token always keys the same record the string spellings do. + */ + constructor(description: string) { + const name = description[0] === "$" ? description.slice(1) : description; + mintTokenName(name, this); + Object.defineProperty(this, INJECTION_TOKEN_NAME, { + value: name, + writable: false, + enumerable: false, + configurable: false, + }); + } + + public get description(): string { + return (this)[INJECTION_TOKEN_NAME]; + } + + public toString(): string { + return `InjectionToken(${this.description})`; + } +} + +/** + * Reads the constructor-set name. Own-property check, and by marker rather + * than `instanceof`, so tokens minted by a duplicated copy of this module are + * still recognized. + */ +export function getInjectionTokenName(token: any): string | undefined { + if ( + token !== null && + typeof token === "object" && + Object.prototype.hasOwnProperty.call(token, INJECTION_TOKEN_NAME) + ) { + return token[INJECTION_TOKEN_NAME]; + } + return undefined; +} diff --git a/lib/common/di/injector.ts b/lib/common/di/injector.ts new file mode 100644 index 0000000000..3f5a1f2470 --- /dev/null +++ b/lib/common/di/injector.ts @@ -0,0 +1,435 @@ +import { annotate } from "../helpers"; +import { getContractName } from "./contract"; +import { resolveForwardRef } from "./forward-ref"; +import { runInInjectionContext } from "./inject"; +import { getInjectionTokenName } from "./injection-token"; +import type { + InternalProvider, + Provider, + ProviderToken, + Type, +} from "./providers"; + +type TokenKey = string | object; + +export interface InjectOptions { + /** Resolve to null instead of throwing when the token is not registered. */ + optional?: boolean; + /** Resolve at this injector's level only — no parent fallthrough. */ + self?: boolean; + /** + * Start resolution at the parent — escapes a child scope's shadowing + * entry (e.g. a hook payload key that collides with a service name). + */ + skipSelf?: boolean; +} + +type ProviderKind = "value" | "class" | "factory" | "lazyClass" | "legacyClass"; + +interface IProviderRecord { + displayName: string; + kind?: ProviderKind; + shared: boolean; + useValue?: any; + useClass?: Type; + useFactory?: () => any; + useLazyClass?: () => Type; + useLegacyClass?: Function; + /** + * Deferred side-effect loader (Yok's require path). Consumed on first + * resolution; it is expected to register a real resolver onto this record. + */ + pendingLoader?: () => void; + /** Every produced instance is retained, transients included — dispose() walks them. */ + instances: any[]; + constructing: boolean; +} + +// Shared across the whole injector tree so cycle reports show the full path +// even when resolution hops between parent and child scopes. +const resolutionStack: string[] = []; + +export class Injector { + private providers = new Map(); + private instantiationOrder: any[] = []; + + constructor( + providers: Provider[] = [], + private parent?: Injector, + ) { + this.register({ provide: Injector, useValue: this }); + this.register(providers); + } + + /** + * Resolution is two-step per injector level: the token itself, then its + * decorator-set name — and only on a full local miss does lookup delegate + * to the parent. The per-level order is what lets a child scope's + * string-keyed entry (a per-call override, a hook payload) shadow a parent + * provider for class-token consumers too. + * + * `ctorArguments` is the legacy per-call bag: raw Yok semantics (own-key + * check, no `$` normalization), applied only to the construction the call + * itself triggers — it never propagates to nested resolutions. + */ + // T defaults to any so string tokens (which give inference no source and + // would otherwise land on unknown) stay ergonomic during the migration. + public get(token: ProviderToken): T; + public get( + token: ProviderToken, + options: InjectOptions & { optional: true }, + ): T | null; + public get(token: ProviderToken, options: InjectOptions): T; + public get( + token: ProviderToken, + options?: InjectOptions, + ): T | null { + if (options && options.self && options.skipSelf) { + throw new Error("inject options cannot combine self and skipSelf"); + } + token = resolveForwardRef(token); + const found = this.findRecord(token, options); + if (!found) { + if (options && options.optional) { + return null; + } + throw new Error("unable to resolve " + displayNameOf(token)); + } + return found.owner.instantiate(found.record); + } + + /** + * The legacy facade's channel for Yok's `resolve(name, bag)` sites: the + * bag applies to the construction this call itself triggers, with raw + * own-key semantics, and never propagates to nested resolutions. Not part + * of the public API — new code passes per-call providers to + * createInstance instead. + */ + protected getWithLegacyArguments( + token: ProviderToken, + ctorArguments?: { [key: string]: any }, + ): any { + token = resolveForwardRef(token); + const found = this.findRecord(token); + if (!found) { + throw new Error("unable to resolve " + displayNameOf(token)); + } + return found.owner.instantiate(found.record, ctorArguments); + } + + public createChild(providers: Provider[] = []): Injector { + return new Injector(providers, this); + } + + /** + * Constructs a class that need not be registered, resolving its annotated + * parameters against this injector (plus the given per-call providers, + * which shadow one level deep only). Products are deliberately NOT + * retained for disposal — Yok never retained by-class resolutions either. + */ + public createInstance( + cls: Type | Function, + providers: Provider[] = [], + ctorArguments?: { [key: string]: any }, + ): T { + const scope = providers.length ? this.createChild(providers) : this; + return scope.constructLegacy(cls, ctorArguments); + } + + /** Merge-mutate: re-registering a key updates the existing record in place. */ + public register(providers: InternalProvider | InternalProvider[]): void { + const list = Array.isArray(providers) ? providers : [providers]; + for (const provider of list) { + const keys = this.keysFor(provider.provide); + let record: IProviderRecord | undefined; + for (const key of keys) { + record = this.providers.get(key); + if (record) { + break; + } + } + if (!record) { + record = { + displayName: displayNameOf(provider.provide), + shared: true, + instances: [], + constructing: false, + }; + } + this.applyProvider(record, provider); + for (const key of keys) { + this.providers.set(key, record); + } + } + } + + /** Own-level string keys, optionally filtered by prefix — feeds command-name enumeration. */ + public getRegisteredNames(prefix: string = ""): string[] { + const names: string[] = []; + for (const key of this.providers.keys()) { + if (typeof key === "string" && key.startsWith(prefix)) { + names.push(key); + } + } + return names; + } + + public has(token: ProviderToken): boolean { + return !!this.findRecord(token); + } + + /** + * Whether the token can actually produce a value. A record carrying only a + * pending loader answers `has()` but resolves to an error, so the deferred + * paths use this to tell "loaded and registered" from "loaded and silent". + */ + protected hasResolver(token: ProviderToken): boolean { + const found = this.findRecord(token); + return !!found && found.record.kind !== undefined; + } + + /** First cached instance for a token, without triggering construction. */ + public peek(token: ProviderToken): any { + const found = this.findRecord(token); + return found ? found.record.instances[0] : undefined; + } + + /** Deletes the local record under every key that aliases it. */ + public remove(token: ProviderToken): void { + const record = this.findRecordLocal(token); + if (!record) { + return; + } + const keys: TokenKey[] = []; + for (const [key, value] of this.providers) { + if (value === record) { + keys.push(key); + } + } + for (const key of keys) { + this.providers.delete(key); + } + } + + /** + * Reverse instantiation order, then registration-provided values. Sync, + * like Yok's — the exit paths that call this do not await it. + */ + public dispose(exclude: any[] = []): void { + const seen = new Set(exclude); + const disposeOne = (instance: any) => { + if (!instance || seen.has(instance) || instance === this) { + return; + } + seen.add(instance); + if (typeof instance.dispose === "function") { + instance.dispose(); + } + }; + + for (let i = this.instantiationOrder.length - 1; i >= 0; i--) { + disposeOne(this.instantiationOrder[i]); + } + for (const record of new Set(this.providers.values())) { + for (const instance of record.instances) { + disposeOne(instance); + } + } + } + + private keysFor(token: ProviderToken): TokenKey[] { + token = resolveForwardRef(token); + if (typeof token === "string") { + return [normalizeName(token)]; + } + const name = tokenNameOf(token); + return name !== undefined ? [token, name] : [token]; + } + + private applyProvider( + record: IProviderRecord, + provider: InternalProvider, + ): void { + record.shared = provider.shared === undefined ? true : provider.shared; + + if ("useLazyRequire" in provider) { + record.pendingLoader = provider.useLazyRequire; + return; + } + + record.useValue = undefined; + record.useClass = undefined; + record.useFactory = undefined; + record.useLazyClass = undefined; + record.useLegacyClass = undefined; + + if ("useValue" in provider) { + record.kind = "value"; + record.useValue = provider.useValue; + if (record.shared) { + record.instances[0] = provider.useValue; + } else { + record.instances.push(provider.useValue); + } + } else if ("useClass" in provider) { + record.kind = "class"; + record.useClass = provider.useClass; + } else if ("useFactory" in provider) { + record.kind = "factory"; + record.useFactory = provider.useFactory; + } else if ("useLazyClass" in provider) { + record.kind = "lazyClass"; + record.useLazyClass = provider.useLazyClass; + } else if ("useLegacyClass" in provider) { + record.kind = "legacyClass"; + record.useLegacyClass = provider.useLegacyClass; + } + } + + private findRecordLocal(token: ProviderToken): IProviderRecord | undefined { + token = resolveForwardRef(token); + if (typeof token === "string") { + return this.providers.get(normalizeName(token)); + } + const direct = this.providers.get(token); + if (direct) { + return direct; + } + const name = tokenNameOf(token); + return name !== undefined ? this.providers.get(name) : undefined; + } + + // self/skipSelf apply to the entry level only: the parent walk below is + // always an ordinary full lookup from that injector on. + private findRecord( + token: ProviderToken, + options?: InjectOptions, + ): { record: IProviderRecord; owner: Injector } | undefined { + if (!options || !options.skipSelf) { + const local = this.findRecordLocal(token); + if (local) { + return { record: local, owner: this }; + } + if (options && options.self) { + return undefined; + } + } + return this.parent ? this.parent.findRecord(token) : undefined; + } + + private instantiate( + record: IProviderRecord, + ctorArguments?: { [key: string]: any }, + ): any { + if (record.pendingLoader) { + const loader = record.pendingLoader; + // Cleared only after a successful run so a failing loader (missing + // module, broken require) is retried on the next resolution. + loader(); + record.pendingLoader = undefined; + } + + if (record.shared && record.instances.length) { + return record.instances[0]; + } + + if ( + record.kind === undefined || + (record.kind === "value" && !record.shared) + ) { + throw new Error("no resolver registered for " + record.displayName); + } + + if (record.kind === "value") { + return record.instances[0]; + } + + if (record.constructing) { + const cyclePath = resolutionStack.concat(record.displayName).join(" -> "); + throw new Error( + `Cyclic dependency detected on dependency '${record.displayName}'. Resolution path: ${cyclePath}`, + ); + } + + record.constructing = true; + resolutionStack.push(record.displayName); + let instance: any; + try { + instance = this.construct(record, ctorArguments); + } finally { + resolutionStack.pop(); + record.constructing = false; + } + + record.instances.push(instance); + this.instantiationOrder.push(instance); + return instance; + } + + private construct( + record: IProviderRecord, + ctorArguments?: { [key: string]: any }, + ): any { + switch (record.kind) { + case "class": + return runInInjectionContext(this, () => new record.useClass()); + case "factory": + return runInInjectionContext(this, () => record.useFactory()); + case "lazyClass": { + if (!record.useClass) { + record.useClass = record.useLazyClass(); + } + const cls = record.useClass; + return runInInjectionContext(this, () => new cls()); + } + case "legacyClass": + return this.constructLegacy(record.useLegacyClass, ctorArguments); + } + } + + private constructLegacy( + ctor: any, + ctorArguments?: { [key: string]: any }, + ): any { + annotate(ctor); + + const resolvedArgs = ctor.$inject.args.map((paramName: string) => + ctorArguments && + Object.prototype.hasOwnProperty.call(ctorArguments, paramName) + ? ctorArguments[paramName] + : this.get(paramName), + ); + + const name = ctor.$inject.name; + return runInInjectionContext(this, () => + name && name[0] === name[0].toUpperCase() + ? new ctor(...resolvedArgs) + : ctor.apply(null, resolvedArgs), + ); + } +} + +function normalizeName(name: string): string { + return name[0] === "$" ? name.slice(1) : name; +} + +/** The name a non-string token aliases in the legacy registry, if it has one. */ +function tokenNameOf(token: ProviderToken): string | undefined { + const injectionTokenName = getInjectionTokenName(token); + return injectionTokenName !== undefined + ? injectionTokenName + : getContractName(token); +} + +function displayNameOf(token: ProviderToken): string { + if (typeof token === "string") { + return normalizeName(token); + } + const injectionTokenName = getInjectionTokenName(token); + if (injectionTokenName !== undefined) { + return `InjectionToken(${injectionTokenName})`; + } + return ( + getContractName(token) || (token).name || "" + ); +} diff --git a/lib/common/di/providers.ts b/lib/common/di/providers.ts new file mode 100644 index 0000000000..61c8d738d4 --- /dev/null +++ b/lib/common/di/providers.ts @@ -0,0 +1,70 @@ +import type { InjectionToken } from "./injection-token"; + +export type Type = new (...args: any[]) => T; +export type AbstractType = abstract new (...args: any[]) => T; + +export type ProviderToken = + string | Type | AbstractType | InjectionToken; + +interface IBaseProvider { + provide: ProviderToken; + /** Defaults to true. `false` constructs a fresh instance per resolution. */ + shared?: boolean; +} + +export interface IClassProvider extends IBaseProvider { + useClass: Type; +} + +export interface IValueProvider extends IBaseProvider { + useValue: T; +} + +export interface IFactoryProvider extends IBaseProvider { + useFactory: () => T; +} + +/** The loader runs on first resolution only — module loading stays deferred. */ +export interface ILazyClassProvider extends IBaseProvider { + useLazyClass: () => Type; +} + +/** + * Yok-style resolver constructed via annotate(): parameters are resolved by + * name, and lowercase/anonymous functions are invoked as factories rather + * than new-ed. + */ +export interface ILegacyClassProvider extends IBaseProvider { + useLegacyClass: Function; +} + +/** + * Deferred side-effect loader (Yok's `require(name, path)`): running it is + * expected to register the real resolver onto this same record. Container + * internals only — a record left with nothing but a loader resolves to an + * error, so it is deliberately kept out of `Provider`. + */ +export interface ILazyRequireProvider extends IBaseProvider { + useLazyRequire: () => void; +} + +export type Provider = + | IClassProvider + | IValueProvider + | IFactoryProvider + | ILazyClassProvider + | ILegacyClassProvider; + +/** The provider forms the container accepts, including the unpublished ones. */ +export type InternalProvider = Provider | ILazyRequireProvider; + +/** Enforces at compile time that the implementation satisfies the token. */ +export const provide = ( + token: AbstractType | InjectionToken | string, + impl: Type, +): Provider => ({ provide: token, useClass: impl }); + +export const provideLazy = ( + token: AbstractType | InjectionToken | string, + load: () => Type, +): Provider => ({ provide: token, useLazyClass: load }); diff --git a/lib/common/dispatchers.ts b/lib/common/dispatchers.ts index 1fa9edb065..6882f72a09 100644 --- a/lib/common/dispatchers.ts +++ b/lib/common/dispatchers.ts @@ -1,14 +1,10 @@ import * as _ from "lodash"; -import * as queue from "./queue"; import * as path from "path"; import { hook } from "./helpers"; import { ICommandDispatcher, ICancellationService, ISysInfo, - IFutureDispatcher, - IQueue, - IErrors, } from "./declarations"; import { IOptions, IPackageManager, IVersionsService } from "../declarations"; import { IInjector } from "./definitions/yok"; @@ -29,7 +25,7 @@ export class CommandDispatcher implements ICommandDispatcher { private $options: IOptions, private $versionsService: IVersionsService, private $packageManager: IPackageManager, - private $terminalSpinnerService: ITerminalSpinnerService + private $terminalSpinnerService: ITerminalSpinnerService, ) {} public async dispatchCommand(): Promise { @@ -45,7 +41,7 @@ export class CommandDispatcher implements ICommandDispatcher { __dirname, "..", "..", - "package.json" + "package.json", ), }); this.$logger.trace("System information:"); @@ -76,7 +72,7 @@ export class CommandDispatcher implements ICommandDispatcher { await this.$commandsService.tryExecuteCommand( commandName, - commandArguments + commandArguments, ); } @@ -84,7 +80,7 @@ export class CommandDispatcher implements ICommandDispatcher { private async resolveCommand( commandName: string, commandArguments: string[], - argv: string[] + argv: string[], ) { // just a hook point return { commandName, commandArguments, argv }; @@ -142,39 +138,16 @@ export class CommandDispatcher implements ICommandDispatcher { nativescriptCliVersion.latestVersion, { loose: true, - } + }, ) ) { // up-to-date spinner.succeed("Up to date."); } else { spinner.info( - `New version of NativeScript CLI is available (${nativescriptCliVersion.latestVersion}), run '${updateCommand}' to update.` + `New version of NativeScript CLI is available (${nativescriptCliVersion.latestVersion}), run '${updateCommand}' to update.`, ); } } } injector.register("commandDispatcher", CommandDispatcher); - -class FutureDispatcher implements IFutureDispatcher { - private actions: IQueue; - - public constructor(private $errors: IErrors) {} - - public async run(): Promise { - if (this.actions) { - this.$errors.fail("You cannot run a running future dispatcher."); - } - this.actions = new queue.Queue(); - - while (true) { - const action = await this.actions.dequeue(); - await action(); - } - } - - public dispatch(action: () => Promise) { - this.actions.enqueue(action); - } -} -injector.register("dispatcher", FutureDispatcher, false); diff --git a/lib/common/enums.ts b/lib/common/enums.ts new file mode 100644 index 0000000000..f432f19269 --- /dev/null +++ b/lib/common/enums.ts @@ -0,0 +1,84 @@ +export enum GoogleAnalyticsDataType { + Page = "pageview", + Event = "event", +} + +export enum TrackingTypes { + /** + * Defines that the data contains information for initialization of a new Analytics monitor. + */ + Initialization = "initialization", + + /** + * Defines that the data contains exception that should be tracked. + */ + Exception = "exception", + + /** + * Defines that the data contains the answer of the question if user allows to be tracked. + */ + AcceptTrackFeatureUsage = "acceptTrackFeatureUsage", + + /** + * Defines data that will be tracked to Google Analytics. + */ + GoogleAnalyticsData = "googleAnalyticsData", + + /** + * Defines that the broker process should send all the pending information to Analytics. + * After that the process should send information it has finished tracking and die gracefully. + */ + FinishTracking = "FinishTracking", +} + +export enum AnalyticsStatus { + /** + * User has allowed to be tracked. + */ + enabled = "enabled", + + /** + * User has declined to be tracked. + */ + disabled = "disabled", + + /** + * User has not been asked to allow feature and error tracking. + */ + notConfirmed = "not confirmed", +} + +export enum OptionType { + /** + * String option + */ + String = "string", + /** + * Boolean option + */ + Boolean = "boolean", + /** + * Number option + */ + Number = "number", + /** + * Array option + */ + Array = "array", + /** + * Object option + */ + Object = "object", +} + +export enum ErrorCodes { + UNCAUGHT = 120, + UNKNOWN = 127, + INVALID_ARGUMENT = 128, + RESOURCE_PROBLEM = 129, + KARMA_FAIL = 130, + UNHANDLED_REJECTION_FAILURE = 131, + DELETED_KILL_FILE = 132, + TESTS_INIT_REQUIRED = 133, + ALL_DEVICES_DISCONNECTED = 134, +} diff --git a/lib/common/errors.ts b/lib/common/errors.ts index b75c8d759b..f0b9f489d8 100644 --- a/lib/common/errors.ts +++ b/lib/common/errors.ts @@ -4,7 +4,8 @@ import * as _ from "lodash"; import { SourceMapConsumer } from "source-map"; import { isInteractive } from "./helpers"; import { deprecated } from "./decorators"; -import { ErrorCodes, IErrors, IFailOptions } from "./declarations"; +import { IErrors, IFailOptions } from "./declarations"; +import { ErrorCodes } from "./enums"; import { IInjector } from "./definitions/yok"; import { injector } from "./yok"; @@ -64,7 +65,7 @@ async function resolveCallStack(error: Error): Promise { functionName, source, sourcePos.line, - sourcePos.column + sourcePos.column, ); } @@ -73,10 +74,10 @@ async function resolveCallStack(error: Error): Promise { functionName, fileName, line, - column + column, ); }); - }) + }), ); let outputMessage = remapped.join("\n"); @@ -90,7 +91,7 @@ async function resolveCallStack(error: Error): Promise { } export function installUncaughtExceptionListener( - actionOnException?: () => void + actionOnException?: () => void, ): void { const handler = async (err: Error) => { try { @@ -123,7 +124,7 @@ export function installUncaughtExceptionListener( async function tryTrackException( error: Error, - localInjector: IInjector + localInjector: IInjector, ): Promise { let disableAnalytics: boolean; try { @@ -190,13 +191,13 @@ export class Errors implements IErrors { exception.name = opts.name || "Exception"; exception.message = util.format.apply( null, - [opts.formatStr].concat(argsArray) + [opts.formatStr].concat(argsArray), ); try { const $messagesService = this.$injector.resolve("messagesService"); exception.message = $messagesService.getMessage.apply( $messagesService, - [opts.formatStr].concat(argsArray) + [opts.formatStr].concat(argsArray), ); } catch (err) { // Ignore @@ -214,7 +215,7 @@ export class Errors implements IErrors { public async beginCommand( action: () => Promise, - printCommandHelpSuggestion: () => Promise + printCommandHelpSuggestion: () => Promise, ): Promise { try { return await action(); @@ -228,8 +229,8 @@ export class Errors implements IErrors { const message = printCallStack ? await resolveCallStack(ex) : isInteractive() - ? `\x1B[31;1m${ex.message}\x1B[0m` - : ex.message; + ? `\x1B[31;1m${ex.message}\x1B[0m` + : ex.message; if (ex.printOnStdout) { logger.info(message); @@ -243,7 +244,7 @@ export class Errors implements IErrors { await tryTrackException(ex, this.$injector); process.exit( - _.isNumber(ex.errorCode) ? ex.errorCode : ErrorCodes.UNKNOWN + _.isNumber(ex.errorCode) ? ex.errorCode : ErrorCodes.UNKNOWN, ); } } diff --git a/lib/common/helpers.ts b/lib/common/helpers.ts index 86e0f3f722..878fcb9206 100644 --- a/lib/common/helpers.ts +++ b/lib/common/helpers.ts @@ -362,6 +362,18 @@ export function toBoolean(str: any): boolean { return !!(str && str.toString && str.toString().toLowerCase() === "true"); } +/** + * Reads an opt-in environment flag: any value other than empty / `0` / + * `false` / `off` / `no` turns it on. + */ +export function isTruthyEnvFlag(value: string | undefined): boolean { + if (typeof value !== "string") { + return false; + } + const v = value.trim().toLowerCase(); + return !!v && v !== "0" && v !== "false" && v !== "off" && v !== "no"; +} + export function block(operation: () => void): void { if (isInteractive()) { (process.stdin).setRawMode(false); @@ -536,9 +548,19 @@ export function decorateMethod( const replacementMethods = _.filter(newMethods, (f) => _.isFunction(f)); if (replacementMethods.length > 0) { hasBeenReplaced = true; + // Each link passes the args it was invoked with down the chain, so + // any middleware's next(...newArgs) — not just the innermost one's — + // is seen by the rest of the chain; next() with no arguments keeps + // the current args. const chainedReplacementMethod = _.reduce( replacementMethods, - (prev, next) => next.bind(next, args, prev), + (prev: Function, next: Function) => + (...forwardedArgs: any[]) => + next.call( + next, + forwardedArgs.length ? forwardedArgs : args, + prev, + ), sink.bind(this), ); result = chainedReplacementMethod(); @@ -558,11 +580,20 @@ export function decorateMethod( }; } +/** + * @deprecated Emits the param-name hook payload contract (keyed off the + * decorated method's parameter names); slated for replacement by a typed + * hook API. + */ export function hook(commandName: string) { function getHooksService(self: any): IHooksService { let hooksService: IHooksService = self.$hooksService; if (!hooksService) { - const injector = self.$injector; + // The process-wide injector must stay the LAST resort: tests stub + // self.$hooksService / self.$injector, and a class migrated off + // property injection has neither — only then may it be used. It is + // required at call time because yok imports this module (cycle). + const injector = self.$injector || require("./yok").getInjector(); if (!injector) { throw Error( "Type with hooks needs to have either $hooksService or $injector injected.", @@ -596,6 +627,7 @@ export function hook(commandName: string) { return hooksService.executeBeforeHooks( commandName, prepareArguments(method, args, hooksService), + { consumesMiddlewares: true }, ); }, async (method: any, self: any, resultPromise: any, args: any[]) => { @@ -850,6 +882,12 @@ const FN_NAME_AND_ARGS = const FN_ARG_SPLIT = /,/; const FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; +/** + * @deprecated Discovers dependencies by regex-parsing constructor source text — + * the reason tests must run against tsc output and the CLI can never be + * bundled. Kept only for the legacy provider kind and param-name hook + * injection; never add new callers. + */ export function annotate(fn: any) { let $inject: any, fnText: string, argDecl: string[]; diff --git a/lib/common/http-client.ts b/lib/common/http-client.ts index 55ac67aa93..3219117377 100644 --- a/lib/common/http-client.ts +++ b/lib/common/http-client.ts @@ -1,6 +1,7 @@ import * as _ from "lodash"; import { EOL } from "os"; import * as util from "util"; +import { pipeline } from "stream/promises"; import { Server, IProxySettings, IProxyService } from "./declarations"; import { injector } from "./yok"; import axios from "axios"; @@ -18,12 +19,12 @@ export class HttpClient implements Server.IHttpClient { constructor( private $logger: ILogger, private $proxyService: IProxyService, - private $staticConfig: Config.IStaticConfig + private $staticConfig: Config.IStaticConfig, ) {} public async httpRequest( options: any, - proxySettings?: IProxySettings + proxySettings?: IProxySettings, ): Promise { try { const result = await this.httpRequestCore(options, proxySettings); @@ -43,7 +44,7 @@ export class HttpClient implements Server.IHttpClient { this.$logger.warn( "%s Retrying request to %s...", err.message, - options.url || options + options.url || options, ); const retryResult = await this.httpRequestCore(options, proxySettings); return { @@ -59,7 +60,7 @@ export class HttpClient implements Server.IHttpClient { private async httpRequestCore( options: any, - proxySettings?: IProxySettings + proxySettings?: IProxySettings, ): Promise { if (_.isString(options)) { options = { @@ -79,7 +80,7 @@ export class HttpClient implements Server.IHttpClient { cliProxySettings, options, headers, - requestProto + requestProto, ); if (!headers["User-Agent"]) { @@ -113,7 +114,11 @@ export class HttpClient implements Server.IHttpClient { method: options.method, proxy: false, httpAgent: agent, + // axios picks the agent by protocol, so an https:// request ignores httpAgent + httpsAgent: agent, data: options.body, + responseType: options.pipeTo ? "stream" : undefined, + onDownloadProgress: options.onDownloadProgress, }).catch((err) => { this.$logger.trace("An error occurred while sending the request:", err); if (err.response) { @@ -137,9 +142,18 @@ export class HttpClient implements Server.IHttpClient { if (result) { this.$logger.trace( "httpRequest: Done. code = %d", - result.status.toString() + result.status.toString(), ); + if (options.pipeTo) { + await pipeline(result.data, options.pipeTo); + + return { + response: result, + headers: result.headers, + }; + } + return { response: result, body: JSON.stringify(result.data), @@ -152,7 +166,7 @@ export class HttpClient implements Server.IHttpClient { if (statusCode === HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED) { const clientNameLowerCase = this.$staticConfig.CLIENT_NAME.toLowerCase(); this.$logger.error( - `You can run ${EOL}\t${clientNameLowerCase} proxy set .${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.` + `You can run ${EOL}\t${clientNameLowerCase} proxy set .${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.`, ); return "Your proxy requires authentication."; } else if (statusCode === HttpStatusCodes.PAYMENT_REQUIRED) { @@ -177,7 +191,7 @@ export class HttpClient implements Server.IHttpClient { } catch (parsingFailed) { this.$logger.trace( "Failed to get error from http request: ", - parsingFailed + parsingFailed, ); return `The server returned unexpected response: ${body}`; } @@ -199,7 +213,7 @@ export class HttpClient implements Server.IHttpClient { cliProxySettings: IProxySettings, options: any, headers: any, - requestProto: string + requestProto: string, ): Promise { const isLocalRequest = options.host === "localhost" || options.host === "127.0.0.1"; diff --git a/lib/common/mobile/android/android-emulator-services.ts b/lib/common/mobile/android/android-emulator-services.ts index 6a973d2ed4..58e1d6b33c 100644 --- a/lib/common/mobile/android/android-emulator-services.ts +++ b/lib/common/mobile/android/android-emulator-services.ts @@ -3,7 +3,7 @@ import { getCurrentEpochTime, sleep } from "../../helpers"; import { EOL } from "os"; import * as _ from "lodash"; import { LoggerConfigData } from "../../../constants"; -import { IChildProcess, IUtils } from "../../declarations"; +import { IChildProcess, IUserSettingsService, IUtils } from "../../declarations"; import { injector } from "../../yok"; import * as semver from "semver"; @@ -18,6 +18,7 @@ export class AndroidEmulatorServices private $emulatorHelper: Mobile.IEmulatorHelper, private $logger: ILogger, private $utils: IUtils, + private $userSettingsService: IUserSettingsService, ) {} public async getEmulatorImages(): Promise { @@ -154,7 +155,7 @@ export class AndroidEmulatorServices }; } - this.spawnEmulator(emulator); + await this.spawnEmulator(emulator); const isInfiniteWait = this.$utils.getMilliSecondsTimeout(timeout) === 0; let hasTimeLeft = getCurrentEpochTime() < endTimeEpoch; @@ -186,7 +187,7 @@ export class AndroidEmulatorServices } } - private spawnEmulator(emulator: Mobile.IDeviceInfo): void { + private async spawnEmulator(emulator: Mobile.IDeviceInfo) { let pathToEmulatorExecutable = null; let startEmulatorArgs = null; if (emulator.vendor === AndroidVirtualDevice.AVD_VENDOR_NAME) { @@ -195,6 +196,12 @@ export class AndroidEmulatorServices startEmulatorArgs = this.$androidVirtualDeviceService.startEmulatorArgs( emulator.imageIdentifier, ); + try { + const additionalArgs = await this.$userSettingsService.getSettingValue("androidEmulatorStartArgs"); + if (additionalArgs?.length) { + startEmulatorArgs.push(...additionalArgs); + } + } catch (error) {} } else if ( emulator.vendor === AndroidVirtualDevice.GENYMOTION_VENDOR_NAME ) { diff --git a/lib/common/mobile/emulator-helper.ts b/lib/common/mobile/emulator-helper.ts index baf98fc7aa..5f00e7246d 100644 --- a/lib/common/mobile/emulator-helper.ts +++ b/lib/common/mobile/emulator-helper.ts @@ -5,7 +5,11 @@ import { injector } from "../yok"; export class EmulatorHelper implements Mobile.IEmulatorHelper { // https://developer.android.com/guide/topics/manifest/uses-sdk-element public mapAndroidApiLevelToVersion = { + "android-37": "17.0.0", + "android-37.0": "17.0.0", + "android-37.1": "17.0.0", "android-36": "16.0.0", + "android-36.1": "16.0.0", "android-35": "15.0.0", "android-34": "14.0.0", "android-33": "13.0.0", diff --git a/lib/common/mobile/ios/device/ios-device-file-system.ts b/lib/common/mobile/ios/device/ios-device-file-system.ts index 705ba0d889..8c507dd0a1 100644 --- a/lib/common/mobile/ios/device/ios-device-file-system.ts +++ b/lib/common/mobile/ios/device/ios-device-file-system.ts @@ -9,12 +9,12 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { private device: Mobile.IDevice, private $logger: ILogger, private $iosDeviceOperations: IIOSDeviceOperations, - private $fs: IFileSystem + private $fs: IFileSystem, ) {} public async listFiles( devicePath: string, - appIdentifier: string + appIdentifier: string, ): Promise { if (!devicePath) { devicePath = "."; @@ -31,10 +31,33 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { this.$logger.info(children.join(EOL)); } + public async getDirectoryEntries( + devicePath: string, + appIdentifier: string, + ): Promise { + try { + const result = await this.$iosDeviceOperations.listDirectory([ + { + deviceId: this.device.deviceInfo.identifier, + path: devicePath, + appId: appIdentifier, + }, + ]); + const entries = + result?.[this.device.deviceInfo.identifier]?.[0]?.response; + return Array.isArray(entries) ? entries : null; + } catch (err) { + this.$logger.trace( + `Unable to list directory '${devicePath}' for application ${appIdentifier}: ${err.message}`, + ); + return null; + } + } + public async getFile( deviceFilePath: string, appIdentifier: string, - outputFilePath?: string + outputFilePath?: string, ): Promise { if (outputFilePath) { await this.$iosDeviceOperations.downloadFiles([ @@ -50,14 +73,14 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { const fileContent = await this.getFileContent( deviceFilePath, - appIdentifier + appIdentifier, ); this.$logger.info(fileContent); } public async getFileContent( deviceFilePath: string, - appIdentifier: string + appIdentifier: string, ): Promise { const result = await this.$iosDeviceOperations.readFiles([ { @@ -73,7 +96,7 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { public async putFile( localFilePath: string, deviceFilePath: string, - appIdentifier: string + appIdentifier: string, ): Promise { await this.uploadFilesCore([ { @@ -86,7 +109,7 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { public async deleteFile( deviceFilePath: string, - appIdentifier: string + appIdentifier: string, ): Promise { await this.$iosDeviceOperations.deleteFiles( [ @@ -98,25 +121,25 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { ], (err: IOSDeviceLib.IDeviceError) => { this.$logger.trace( - `Error while deleting file: ${deviceFilePath}: ${err.message} with code: ${err.code}` + `Error while deleting file: ${deviceFilePath}: ${err.message} with code: ${err.code}`, ); if (err.code !== IOSDeviceFileSystem.AFC_DELETE_FILE_NOT_FOUND_ERROR) { this.$logger.warn( - `Cannot delete file: ${deviceFilePath}. Reason: ${err.message}` + `Cannot delete file: ${deviceFilePath}. Reason: ${err.message}`, ); } - } + }, ); } public async transferFiles( deviceAppData: Mobile.IDeviceAppData, - localToDevicePaths: Mobile.ILocalToDevicePathData[] + localToDevicePaths: Mobile.ILocalToDevicePathData[], ): Promise { const filesToUpload: Mobile.ILocalToDevicePathData[] = _.filter( localToDevicePaths, - (l) => this.$fs.getFsStats(l.getLocalPath()).isFile() + (l) => this.$fs.getFsStats(l.getLocalPath()).isFile(), ); const files: IOSDeviceLib.IFileData[] = filesToUpload.map((l) => ({ source: l.getLocalPath(), @@ -137,7 +160,7 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { public async transferDirectory( deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], - projectFilesPath: string + projectFilesPath: string, ): Promise { await this.transferFiles(deviceAppData, localToDevicePaths); return localToDevicePaths; @@ -145,21 +168,35 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { public async updateHashesOnDevice( hashes: IStringDictionary, - appIdentifier: string + appIdentifier: string, ): Promise { return; } private async uploadFilesCore( - filesToUpload: IOSDeviceLib.IUploadFilesData[] + filesToUpload: IOSDeviceLib.IUploadFilesData[], ): Promise { await this.$iosDeviceOperations.uploadFiles( filesToUpload, (err: IOSDeviceLib.IDeviceError) => { - if (err.deviceId === this.device.deviceInfo.identifier) { + // Previously an error whose deviceId did not exactly match was + // dropped on the floor — including errors with NO deviceId at + // all (some ios-device-lib error paths don't attribute one). + // That left "Successfully synced" printed over a failed + // transfer and the app silently running stale JavaScript. + // Rethrow unless the error is positively attributed to a + // DIFFERENT device; surface even those at warn level so a + // failed upload is never invisible. + if ( + !err.deviceId || + err.deviceId === this.device.deviceInfo.identifier + ) { throw err; } - } + this.$logger.warn( + `File upload error reported for another device (${err.deviceId}): ${err.message}`, + ); + }, ); } } diff --git a/lib/common/mobile/mobile-core/ios-simulator-discovery.ts b/lib/common/mobile/mobile-core/ios-simulator-discovery.ts index d5bf542073..da7ce5c685 100644 --- a/lib/common/mobile/mobile-core/ios-simulator-discovery.ts +++ b/lib/common/mobile/mobile-core/ios-simulator-discovery.ts @@ -6,7 +6,10 @@ import { IInjector } from "../../definitions/yok"; import { injector } from "../../yok"; import * as _ from "lodash"; -export class IOSSimulatorDiscovery extends DeviceDiscovery { +export class IOSSimulatorDiscovery + extends DeviceDiscovery + implements Mobile.IiOSSimulatorDiscovery +{ private cachedSimulators: Mobile.IiSimDevice[] = []; private availableSimulators: IDictionary = {}; @@ -15,13 +18,13 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { private $iOSSimResolver: Mobile.IiOSSimResolver, private $mobileHelper: Mobile.IMobileHelper, private $hostInfo: IHostInfo, - private $iOSEmulatorServices: Mobile.IiOSSimulatorService + private $iOSEmulatorServices: Mobile.IiOSSimulatorService, ) { super(); } public async startLookingForDevices( - options?: Mobile.IDeviceLookingOptions + options?: Mobile.IDeviceLookingOptions, ): Promise { if ( options && @@ -36,7 +39,8 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { private async checkForDevices(): Promise { if (this.$hostInfo.isDarwin) { - const currentSimulators: Mobile.IiSimDevice[] = await this.$iOSSimResolver.iOSSim.getRunningSimulators(); + const currentSimulators: Mobile.IiSimDevice[] = + await this.$iOSSimResolver.iOSSim.getRunningSimulators(); // Remove old simulators _(this.cachedSimulators) @@ -47,8 +51,8 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { simulator && s && simulator.id === s.id && - simulator.state === s.state - ) + simulator.state === s.state, + ), ) .each((s) => this.deleteAndRemoveDevice(s)); @@ -61,8 +65,8 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { simulator && s && simulator.id === s.id && - simulator.state === s.state - ) + simulator.state === s.state, + ), ) .each((s) => this.createAndAddDevice(s)); } @@ -83,7 +87,7 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { if ( !_.find( this.availableSimulators, - (s) => s.imageIdentifier === simulator.imageIdentifier + (s) => s.imageIdentifier === simulator.imageIdentifier, ) ) { lostSimulators.push(simulator); @@ -110,7 +114,7 @@ export class IOSSimulatorDiscovery extends DeviceDiscovery { private createAndAddDevice(simulator: Mobile.IiSimDevice): void { this.cachedSimulators.push(_.cloneDeep(simulator)); this.addDevice( - this.$injector.resolve(IOSSimulator, { simulator: simulator }) + this.$injector.resolve(IOSSimulator, { simulator: simulator }), ); } diff --git a/lib/common/mobile/wp8/wp8-emulator-services.ts b/lib/common/mobile/wp8/wp8-emulator-services.ts deleted file mode 100644 index d36a715894..0000000000 --- a/lib/common/mobile/wp8/wp8-emulator-services.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as path from "path"; -import { IChildProcess } from "../../declarations"; -import { injector } from "../../yok"; - -class Wp8EmulatorServices implements Mobile.IEmulatorPlatformService { - private static WP8_LAUNCHER = "XapDeployCmd.exe"; - private static WP8_LAUNCHER_PATH = - "Microsoft SDKs\\Windows Phone\\v8.0\\Tools\\XAP Deployment"; - - private static get programFilesPath(): string { - return process.arch === "x64" - ? process.env["PROGRAMFILES(X86)"] - : process.env.ProgramFiles; - } - - constructor(private $logger: ILogger, private $childProcess: IChildProcess) {} - - public async getEmulatorId(): Promise { - return ""; - } - - public async getRunningEmulator(image: string): Promise { - return null; - } - - public async getRunningEmulatorImageIdentifier( - emulatorId: string - ): Promise { - return null; - } - - public async getRunningEmulatorIds(): Promise { - return []; - } - - public async startEmulator(): Promise { - return null; - } - - public async runApplicationOnEmulator( - app: string, - emulatorOptions?: Mobile.IRunApplicationOnEmulatorOptions - ): Promise { - this.$logger.info("Starting Windows Phone Emulator"); - const emulatorStarter = this.getPathToEmulatorStarter(); - this.$childProcess - .spawn(emulatorStarter, ["/installlaunch", app, "/targetdevice:xd"], { - stdio: "ignore", - detached: true, - }) - .unref(); - } - - public async getEmulatorImages(): Promise { - return { devices: [], errors: [] }; - } - - public async getRunningEmulators(): Promise { - return []; - } - - public async getRunningEmulatorName(): Promise { - return ""; - } - - private getPathToEmulatorStarter(): string { - return path.join( - Wp8EmulatorServices.programFilesPath, - Wp8EmulatorServices.WP8_LAUNCHER_PATH, - Wp8EmulatorServices.WP8_LAUNCHER - ); - } -} - -injector.register("wp8EmulatorServices", Wp8EmulatorServices); diff --git a/lib/common/queue.ts b/lib/common/queue.ts deleted file mode 100644 index 792d7ce7ed..0000000000 --- a/lib/common/queue.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IQueue } from "./declarations"; - -export class Queue implements IQueue { - private promiseResolve: (value?: void | PromiseLike) => void; - - public constructor(private items?: T[]) { - this.items = this.items === undefined ? [] : this.items; - } - - public enqueue(item: T): void { - this.items.unshift(item); - - if (this.promiseResolve) { - this.promiseResolve(); - } - } - - public async dequeue(): Promise { - if (!this.items.length) { - const promise = new Promise((resolve, reject) => { - this.promiseResolve = resolve; - }); - - await promise; - - this.promiseResolve = null; - } - - return this.items.pop(); - } -} diff --git a/lib/common/services/analytics/google-analytics-custom-dimensions.d.ts b/lib/common/services/analytics/google-analytics-custom-dimensions.ts similarity index 86% rename from lib/common/services/analytics/google-analytics-custom-dimensions.d.ts rename to lib/common/services/analytics/google-analytics-custom-dimensions.ts index 59897cd96e..d87ccf80cc 100644 --- a/lib/common/services/analytics/google-analytics-custom-dimensions.d.ts +++ b/lib/common/services/analytics/google-analytics-custom-dimensions.ts @@ -1,4 +1,4 @@ -declare const enum GoogleAnalyticsCustomDimensions { +export enum GoogleAnalyticsCustomDimensions { cliVersion = "cd1", projectType = "cd2", clientID = "cd3", diff --git a/lib/common/services/cancellation.ts b/lib/common/services/cancellation.ts index 98d25eaacf..f2f94c6522 100644 --- a/lib/common/services/cancellation.ts +++ b/lib/common/services/cancellation.ts @@ -7,8 +7,8 @@ import { IFileSystem, IDictionary, ICancellationService, - ErrorCodes, } from "../declarations"; +import { ErrorCodes } from "../enums"; import { injector } from "../yok"; class CancellationService implements ICancellationService { @@ -17,7 +17,7 @@ class CancellationService implements ICancellationService { constructor( private $fs: IFileSystem, private $logger: ILogger, - private $hostInfo: IHostInfo + private $hostInfo: IHostInfo, ) { if (this.$hostInfo.isWindows) { this.$fs.createDirectory(CancellationService.killSwitchDir); @@ -41,7 +41,7 @@ class CancellationService implements ICancellationService { .watch(triggerFile, { ignoreInitial: true }) .on("unlink", (filePath: string) => { this.$logger.info( - `Exiting process as the file ${filePath} has been deleted. Probably reinstalling CLI while there's a working instance.` + `Exiting process as the file ${filePath} has been deleted. Probably reinstalling CLI while there's a working instance.`, ); process.exit(ErrorCodes.DELETED_KILL_FILE); }); @@ -69,7 +69,7 @@ class CancellationService implements ICancellationService { return path.join( os.tmpdir(), process.env.SUDO_USER || process.env.USER || process.env.USERNAME || "", - "KillSwitches" + "KillSwitches", ); } diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts new file mode 100644 index 0000000000..5ccefc5116 --- /dev/null +++ b/lib/common/services/command-definition-adapter.ts @@ -0,0 +1,244 @@ +import { OptionType } from "../enums"; +import { injector } from "../yok"; +import { runInInjectionContext } from "../di/inject"; +import { IDictionary, IDashedOption, IErrors } from "../declarations"; +import { IInjector } from "../definitions/yok"; +import { ICommand } from "../definitions/commands"; +import { CommandRegistry } from "../contracts/command-registry"; +import { + CommandContext, + CommandDefinition, + CommandOptionType, + CommandOptionsSchema, + DefinedCommand, + isCommandDefinition, +} from "../define-command"; + +const OPTION_TYPES: IDictionary = { + boolean: OptionType.Boolean, + string: OptionType.String, + number: OptionType.Number, + array: OptionType.Array, +}; + +const compileOptions = ( + schema: CommandOptionsSchema, +): IDictionary => { + const dashedOptions: IDictionary = {}; + + for (const optionName of Object.keys(schema)) { + const spec = schema[optionName]; + const dashedOption: IDashedOption = { + type: OPTION_TYPES[spec.type], + hasSensitiveValue: spec.hasSensitiveValue === true, + }; + + if (spec.default !== undefined) { + dashedOption.default = spec.default; + } + + if (spec.alias !== undefined) { + dashedOption.alias = spec.alias; + } + + if (spec.description !== undefined) { + dashedOption.describe = spec.description; + } + + dashedOptions[optionName] = dashedOption; + } + + return dashedOptions; +}; + +const aliasList = (alias: string | string[]): string[] => + alias === undefined ? [] : Array.isArray(alias) ? alias : [alias]; + +/** + * A command option that shadows a CLI-wide one wins the re-parse for this + * command only, so the same spelling means different things depending on which + * command is running. Warned rather than rejected while the policy is open. + */ +const warnOnCliOptionCollisions = ( + targetInjector: IInjector, + definition: CommandDefinition, + schema: CommandOptionsSchema, + optionsService: any, +): void => { + const cliOptions = optionsService && optionsService.options; + if (!cliOptions) { + return; + } + + // Every spelling the CLI already answers to, mapped to the option owning it. + const cliSpellings: IDictionary = {}; + for (const cliName of Object.keys(cliOptions)) { + cliSpellings[cliName] = cliName; + for (const alias of aliasList(cliOptions[cliName].alias)) { + cliSpellings[alias] = cliName; + } + } + + const collisions: string[] = []; + for (const optionName of Object.keys(schema)) { + if (cliSpellings[optionName]) { + collisions.push( + `'--${optionName}' with the CLI option '--${cliSpellings[optionName]}'`, + ); + } + + for (const alias of aliasList(schema[optionName].alias)) { + if (cliSpellings[alias]) { + collisions.push( + `alias '-${alias}' of '--${optionName}' with the CLI option '--${cliSpellings[alias]}'`, + ); + } + } + } + + if (!collisions.length) { + return; + } + + const logger = targetInjector.get("logger", { optional: true }); + if (!logger) { + return; + } + + const commandName = Array.isArray(definition.name) + ? definition.name[0] + : definition.name; + logger.warn( + `Command '${commandName}' declares options that collide with CLI-wide ` + + `ones: ${collisions.join("; ")}. The command's declaration wins while ` + + `the command runs; rename them to avoid it.`, + ); +}; + +/** + * Wraps a declarative definition in the ICommand shape the legacy registry and + * CommandsService expect. + * + * The compiled command always exposes `canExecute`, because CommandsService + * skips `allowedParameters` entirely once it is present: the adapter enforces + * the declared `arguments` policy itself and only then consults the + * definition's own `canExecute`, so the two fields compose. + */ +export function createCommandFromDefinition< + TSchema extends CommandOptionsSchema, +>( + definition: CommandDefinition, + targetInjector: IInjector = injector, +): ICommand { + const schema = definition.options || {}; + const optionNames = Object.keys(schema); + const dashedOptions = compileOptions(schema); + + // Only a definition that declares options may depend on the options service + // being registered - a bare command must work without one. + const optionsService: any = optionNames.length + ? targetInjector.resolve("options") + : null; + + warnOnCliOptionCollisions(targetInjector, definition, schema, optionsService); + + const commandName = Array.isArray(definition.name) + ? definition.name[0] + : definition.name; + + const fail = (message: string): never => { + if (typeof message !== "string" || !message.trim()) { + throw new Error( + `ctx.fail() for command '${commandName}' requires a non-empty message.`, + ); + } + + const errors: IErrors = targetInjector.resolve("errors"); + return errors.failWithHelp(message); + }; + + // Read per call rather than snapshotted here: the options service only holds + // this command's parsed values once validateOptions has run for it. + const buildContext = (args: string[]): CommandContext => { + const options: any = {}; + for (const optionName of optionNames) { + options[optionName] = optionsService[optionName]; + } + + return { args, options, fail }; + }; + + const acceptsArguments = definition.arguments === "any"; + + return { + allowedParameters: [], + dashedOptions, + ...(definition.disableAnalytics === undefined + ? {} + : { disableAnalytics: definition.disableAnalytics }), + ...(definition.enableHooks === undefined + ? {} + : { enableHooks: definition.enableHooks }), + canExecute: async (args: string[]): Promise => { + if (!acceptsArguments && args.length) { + fail("This command doesn't accept parameters."); + } + + const refine = definition.canExecute; + if (!refine) { + return true; + } + + // Same first-await rule as execute: runInInjectionContext is + // synchronous, so inject() is available up to the first await. + return await runInInjectionContext(targetInjector, () => + refine.call(definition, buildContext(args)), + ); + }, + execute: async (args: string[]): Promise => { + await runInInjectionContext(targetInjector, () => + definition.run(buildContext(args)), + ); + }, + }; +} + +/** + * Registers a definition under an externally chosen command name. Extension + * manifests route by their own key, which need not be the definition's own + * name, so the name is a parameter rather than read off the definition. + */ +export function registerDefinitionAs( + name: string, + definition: DefinedCommand, + targetInjector: IInjector = injector, +): void { + // The registry facet rather than the injector itself, so a child injector + // that provides its own CommandRegistry receives the registration. + const registry = targetInjector.get(CommandRegistry); + // A prototype-less zero-parameter function registers as a useFactory + // provider, so the command is built on first resolution and cached. + registry.registerCommand(name, () => + createCommandFromDefinition(definition, targetInjector), + ); +} + +export function registerCommandDefinition( + definition: DefinedCommand, + targetInjector: IInjector = injector, +): void { + if (!isCommandDefinition(definition)) { + throw new Error( + "registerCommandDefinition() takes the result of defineCommand(); " + + "the value passed carries no command-definition marker.", + ); + } + + const names = Array.isArray(definition.name) + ? definition.name + : [definition.name]; + + for (const name of names) { + registerDefinitionAs(name, definition, targetInjector); + } +} diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index 898909c09e..37aeab6242 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -4,12 +4,8 @@ import { CommandsDelimiters } from "../constants"; import { EOL } from "os"; import * as _ from "lodash"; import { IOptions, IOptionsTracker } from "../../declarations"; -import { - IErrors, - IHooksService, - IAnalyticsService, - GoogleAnalyticsDataType, -} from "../declarations"; +import { IErrors, IHooksService, IAnalyticsService } from "../declarations"; +import { GoogleAnalyticsDataType } from "../enums"; import { IInjector } from "../definitions/yok"; import { injector } from "../yok"; import { IExtensibilityService } from "../definitions/extensibility"; @@ -21,7 +17,10 @@ import { } from "../definitions/commands"; class CommandArgumentsValidationHelper { - constructor(public isValid: boolean, _remainingArguments: string[]) { + constructor( + public isValid: boolean, + _remainingArguments: string[], + ) { this.remainingArguments = _remainingArguments.slice(); } @@ -43,19 +42,19 @@ export class CommandsService implements ICommandsService { private $options: IOptions, private $staticConfig: Config.IStaticConfig, private $extensibilityService: IExtensibilityService, - private $optionsTracker: IOptionsTracker + private $optionsTracker: IOptionsTracker, ) {} public allCommands(opts: { includeDevCommands: boolean }): string[] { const commands = this.$injector.getRegisteredCommandsNames( - opts.includeDevCommands + opts.includeDevCommands, ); return _.reject(commands, (command) => _.includes(command, "|")); } public async executeCommandUnchecked( commandName: string, - commandArguments: string[] + commandArguments: string[], ): Promise { this.commands.push({ commandName, commandArguments }); const command = this.$injector.resolveCommand(commandName); @@ -70,7 +69,7 @@ export class CommandsService implements ICommandsService { await analyticsService.checkConsent(); const beautifiedCommandName = this.beautifyCommandName( - commandName + commandName, ).replace(/\|/g, " "); const googleAnalyticsPageData: IGoogleAnalyticsPageviewData = { @@ -90,18 +89,18 @@ export class CommandsService implements ICommandsService { // Handle correctly hierarchical commands const hierarchicalCommandName = this.$injector.buildHierarchicalCommand( commandName, - commandArguments + commandArguments, ); if (hierarchicalCommandName) { commandName = helpers.stringReplaceAll( hierarchicalCommandName.commandName, CommandsDelimiters.DefaultHierarchicalCommand, - CommandsDelimiters.HooksCommand + CommandsDelimiters.HooksCommand, ); commandName = helpers.stringReplaceAll( commandName, CommandsDelimiters.HierarchicalCommand, - CommandsDelimiters.HooksCommand + CommandsDelimiters.HooksCommand, ); } @@ -130,12 +129,12 @@ export class CommandsService implements ICommandsService { ? helpers.stringReplaceAll( this.beautifyCommandName(commandName), "|", - " " - ) + " " + " ", + ) + " " : ""; const commandHelp = `ns ${command}--help`; this.$logger.printMarkdown( - `__Run \`${commandHelp}\` for more information.__` + `__Run \`${commandHelp}\` for more information.__`, ); return; } @@ -145,21 +144,24 @@ export class CommandsService implements ICommandsService { commandArguments: string[], action: ( _commandName: string, - _commandArguments: string[] - ) => Promise + _commandArguments: string[], + ) => Promise, ): Promise { return this.$errors.beginCommand( () => action.apply(this, [commandName, commandArguments]), - () => this.printHelpSuggestion(commandName) + () => this.printHelpSuggestion(commandName), ); } private async tryExecuteCommandAction( commandName: string, - commandArguments: string[] + commandArguments: string[], ): Promise { const command = this.$injector.resolveCommand(commandName); - if (!command || !command.isHierarchicalCommand) { + if ( + !command || + (!command.isHierarchicalCommand && !command.skipOptionsValidation) + ) { const dashedOptions = command ? command.dashedOptions : null; this.$options.validateOptions(dashedOptions); } @@ -169,12 +171,12 @@ export class CommandsService implements ICommandsService { public async tryExecuteCommand( commandName: string, - commandArguments: string[] + commandArguments: string[], ): Promise { const canExecuteResult: any = await this.executeCommandAction( commandName, commandArguments, - this.tryExecuteCommandAction + this.tryExecuteCommandAction, ); const canExecute = typeof canExecuteResult === "object" @@ -185,7 +187,7 @@ export class CommandsService implements ICommandsService { await this.executeCommandAction( commandName, commandArguments, - this.executeCommandUnchecked + this.executeCommandUnchecked, ); } else { // If canExecuteCommand returns false, the command cannot be executed or there's no such command at all. @@ -204,7 +206,7 @@ export class CommandsService implements ICommandsService { private async canExecuteCommand( commandName: string, commandArguments: string[], - isDynamicCommand?: boolean + isDynamicCommand?: boolean, ): Promise { const command = this.$injector.resolveCommand(commandName); const beautifiedName = helpers.stringReplaceAll(commandName, "|", " "); @@ -212,7 +214,7 @@ export class CommandsService implements ICommandsService { // Verify command is enabled if (command.isDisabled) { this.$errors.fail( - "This command is not applicable to your environment." + "This command is not applicable to your environment.", ); } @@ -225,7 +227,7 @@ export class CommandsService implements ICommandsService { if ( await this.$injector.isValidHierarchicalCommand( commandName, - commandArguments + commandArguments, ) ) { return true; @@ -247,7 +249,7 @@ export class CommandsService implements ICommandsService { const extensionData = await this.$extensibilityService.getExtensionNameWhereCommandIsRegistered( - commandInfo + commandInfo, ); if (extensionData) { @@ -263,11 +265,11 @@ export class CommandsService implements ICommandsService { private async validateMandatoryParams( commandArguments: string[], - mandatoryParams: ICommandParameter[] + mandatoryParams: ICommandParameter[], ): Promise { const commandArgsHelper = new CommandArgumentsValidationHelper( true, - commandArguments + commandArguments, ); if (mandatoryParams.length > 0) { @@ -275,12 +277,12 @@ export class CommandsService implements ICommandsService { if (mandatoryParams.length > commandArguments.length) { const customErrorMessages = _.map( mandatoryParams, - (mp) => mp.errorMessage + (mp) => mp.errorMessage, ); customErrorMessages.splice( 0, 0, - "You need to provide all the required parameters." + "You need to provide all the required parameters.", ); this.$errors.failWithHelp(customErrorMessages.join(EOL)); } @@ -308,7 +310,7 @@ export class CommandsService implements ICommandsService { if (argument) { helpers.remove( commandArgsHelper.remainingArguments, - (arg) => arg === argument + (arg) => arg === argument, ); } else { this.$errors.failWithHelp("Missing mandatory parameter."); @@ -321,15 +323,15 @@ export class CommandsService implements ICommandsService { private async validateCommandArguments( command: ICommand, - commandArguments: string[] + commandArguments: string[], ): Promise { const mandatoryParams: ICommandParameter[] = _.filter( command.allowedParameters, - (param) => param.mandatory + (param) => param.mandatory, ); const commandArgsHelper = await this.validateMandatoryParams( commandArguments, - mandatoryParams + mandatoryParams, ); if (!commandArgsHelper.isValid) { return false; @@ -343,7 +345,7 @@ export class CommandsService implements ICommandsService { } else { // Exclude mandatory params, we've already checked them const unverifiedAllowedParams = command.allowedParameters.filter( - (param) => !param.mandatory + (param) => !param.mandatory, ); for ( @@ -372,7 +374,7 @@ export class CommandsService implements ICommandsService { unverifiedAllowedParams.splice(index, 1); } else { this.$errors.failWithHelp( - `The parameter ${argument} is not valid for this command.` + `The parameter ${argument} is not valid for this command.`, ); } } diff --git a/lib/common/services/hooks-service.ts b/lib/common/services/hooks-service.ts index 0b9676258a..c7a843e02d 100644 --- a/lib/common/services/hooks-service.ts +++ b/lib/common/services/hooks-service.ts @@ -2,6 +2,10 @@ import * as path from "path"; import * as util from "util"; import * as _ from "lodash"; import { annotate, getValueFromNestedObject } from "../helpers"; +import { reportDeprecation } from "../deprecation"; +import { createHookInvocation, isHookDefinition } from "../define-hook"; +import type { HookMiddleware, HookDefinition } from "../define-hook"; +import { runInInjectionContext } from "../di/inject"; import { AnalyticsEventLabelDelimiter } from "../../constants"; import { IOptions, IPerformanceService } from "../../declarations"; import { @@ -13,6 +17,7 @@ import { IErrors, IProjectHelper, IStringDictionary, + IHookExecutionOptions, } from "../declarations"; import { INsConfigHooks, @@ -95,10 +100,16 @@ export class HooksService implements IHooksService { public executeBeforeHooks( commandName: string, hookArguments?: IDictionary, - ): Promise { + options?: IHookExecutionOptions, + ): Promise { const beforeHookName = `before-${HooksService.formatHookName(commandName)}`; const traceMessage = `BeforeHookName for command ${commandName} is ${beforeHookName}`; - return this.executeHooks(beforeHookName, traceMessage, hookArguments); + return this.executeHooks( + beforeHookName, + traceMessage, + hookArguments, + !!(options && options.consumesMiddlewares), + ); } public executeAfterHooks( @@ -107,13 +118,14 @@ export class HooksService implements IHooksService { ): Promise { const afterHookName = `after-${HooksService.formatHookName(commandName)}`; const traceMessage = `AfterHookName for command ${commandName} is ${afterHookName}`; - return this.executeHooks(afterHookName, traceMessage, hookArguments); + return this.executeHooks(afterHookName, traceMessage, hookArguments, false); } private async executeHooks( hookName: string, traceMessage: string, - hookArguments?: IDictionary, + hookArguments: IDictionary, + consumesMiddlewares: boolean, ): Promise { if (this.$config.DISABLE_HOOKS || !this.$options.hooks) { return; @@ -139,6 +151,7 @@ export class HooksService implements IHooksService { hooksDirectory, hookName, hookArguments, + consumesMiddlewares, ), ); } @@ -152,6 +165,7 @@ export class HooksService implements IHooksService { hookName, hook, hookArguments, + consumesMiddlewares, ), ); } @@ -172,7 +186,8 @@ export class HooksService implements IHooksService { directoryPath: string, hookName: string, hook: IHook, - hookArguments?: IDictionary, + hookArguments: IDictionary, + consumesMiddlewares: boolean, ): Promise { hookArguments = hookArguments || {}; @@ -212,62 +227,113 @@ export class HooksService implements IHooksService { const { default: hookFn } = await import(hook.fullPath); hookEntryPoint = hookFn; } else { - hookEntryPoint = require(hook.fullPath); + const hookModule = require(hook.fullPath); + // transpiled ES modules expose the hook as `exports.default`. + hookEntryPoint = + hookModule && typeof hookModule.default === "function" + ? hookModule.default + : hookModule; } - this.$logger.trace(`Validating ${hookName} arguments.`); + // Covers both a `.mjs` default export and tsc's CommonJS emit of + // `export default`, whose value lands under `.default`. + const definitionCandidate = + (hookEntryPoint && hookEntryPoint.default) ?? hookEntryPoint; - const invalidArguments = this.validateHookArguments( - hookEntryPoint, - hook.fullPath, - ); + // Reserved: a future release may accept several definitions from one + // file, so an array must not silently do nothing until then. + if (Array.isArray(definitionCandidate)) { + throw new Error( + `${hook.fullPath} exports an array, which is not a supported hook entry point. Export a single hook definition or function per file.`, + ); + } - if (invalidArguments.length) { + if (isHookDefinition(definitionCandidate)) { + result = await this.executeHookDefinition( + definitionCandidate, + hookName, + hook, + hookArguments, + consumesMiddlewares, + ); + } else if (typeof hookEntryPoint !== "function") { + // A definition is a plain object, so this guard has to stay below the + // definition check. this.$logger.warn( - `${ - hook.fullPath - } will NOT be executed because it has invalid arguments - ${color.grey( - invalidArguments.join(", "), - )}.`, + `${hook.fullPath} will NOT be executed because it does not export a function.`, ); return; - } + } else { + this.$logger.trace(`Validating ${hookName} arguments.`); - // HACK for backwards compatibility: - // In case $projectData wasn't resolved by the time we got here (most likely we got here without running a command but through a service directly) - // then it is probably passed as a hookArg - // if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector - // This helps make hooks stateless - const projectDataHookArg = - hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"]; - if (projectDataHookArg) { - hookArguments["projectData"] = hookArguments["$projectData"] = - projectDataHookArg; - } + const invalidArguments = this.validateHookArguments( + hookEntryPoint, + hook.fullPath, + ); - const maybePromise = this.$injector.resolve( - hookEntryPoint, - hookArguments, - ); - if (maybePromise) { - this.$logger.trace("Hook promises to signal completion"); - try { - result = await maybePromise; - } catch (err) { - if ( - err && - _.isBoolean(err.stopExecution) && - err.errorAsWarning === true - ) { - this.$logger.warn(err.message || err); - } else { - // Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles. - this.$logger.error(err); - throw err || new Error(`Failed to execute hook: ${hook.fullPath}.`); - } + if (invalidArguments.length) { + this.$logger.warn( + `${ + hook.fullPath + } will NOT be executed because it has invalid arguments - ${color.grey( + invalidArguments.join(", "), + )}.`, + ); + return; + } + + // HACK for backwards compatibility: + // In case $projectData wasn't resolved by the time we got here (most likely we got here without running a command but through a service directly) + // then it is probably passed as a hookArg + // if that is the case then pass it directly to the hook instead of trying to resolve $projectData via injector + // This helps make hooks stateless + const projectDataHookArg = + hookArguments["hookArgs"] && hookArguments["hookArgs"]["projectData"]; + if (projectDataHookArg) { + hookArguments["projectData"] = hookArguments["$projectData"] = + projectDataHookArg; + } + + // Only param-name *service* injection is on the deprecation track; a + // hook declaring nothing but `hookArgs` (or no parameters) already + // follows the recommended pattern and must not be flagged. + const usesParamNameInjection = (( + hookEntryPoint.$inject.args + )).some((argument) => argument !== this.hookArgsName); + if (usesParamNameInjection) { + reportDeprecation({ + api: "hooks.param-name-signature", + detail: hook.fullPath, + logger: this.$logger, + }); } - this.$logger.trace("Hook completed"); + const maybePromise = this.$injector.resolve( + hookEntryPoint, + hookArguments, + ); + if (maybePromise) { + this.$logger.trace("Hook promises to signal completion"); + try { + result = await maybePromise; + } catch (err) { + if ( + err && + _.isBoolean(err.stopExecution) && + err.errorAsWarning === true + ) { + this.$logger.warn(err.message || err); + } else { + // Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles. + this.$logger.error(err); + throw ( + err || new Error(`Failed to execute hook: ${hook.fullPath}.`) + ); + } + } + + this.$logger.trace("Hook completed"); + } } } else { const environment = this.prepareEnvironment(hook.fullPath); @@ -306,10 +372,62 @@ export class HooksService implements IHooksService { return result; } + private async executeHookDefinition( + definition: HookDefinition, + hookName: string, + hook: IHook, + hookArguments: IDictionary, + consumesMiddlewares: boolean, + ): Promise { + // The name decides when a hook fires, so a disagreeing one is a mistake + // with no safe reading — running it anyway would fire it at a point its + // author never wrote it for. + if (definition.name !== hookName) { + this.$logger.warn( + `${hook.fullPath} will NOT be executed: it defines the "${definition.name}" hook but is placed at the "${hookName}" hook point.`, + ); + return; + } + + const { context, middlewares } = createHookInvocation(hookArguments, { + hookName, + consumesMiddlewares, + }); + + try { + const returnedValue = await runInInjectionContext(this.$injector, () => + definition.run(context), + ); + + if (typeof returnedValue === "function") { + this.$logger.warn( + `${hook.fullPath} returned a function. Returning a middleware is the legacy convention and is ignored for hook definitions — use ctx.wrap() instead.`, + ); + } + } catch (err) { + if ( + err && + _.isBoolean(err.stopExecution) && + err.errorAsWarning === true + ) { + this.$logger.warn(err.message || err); + } else { + // Print the actual error with its callstack, so it is easy to find out which hooks is causing troubles. + this.$logger.error(err); + throw err || new Error(`Failed to execute hook: ${hook.fullPath}.`); + } + } + + this.$logger.trace("Hook completed"); + + return middlewares.length ? middlewares : undefined; + } + private async executeHooksInDirectory( directoryPath: string, hookName: string, - hookArguments?: IDictionary, + hookArguments: IDictionary, + consumesMiddlewares: boolean, ): Promise { hookArguments = hookArguments || {}; const results: any[] = []; @@ -322,6 +440,7 @@ export class HooksService implements IHooksService { hookName, hook, hookArguments, + consumesMiddlewares, ); if (result) { @@ -329,7 +448,10 @@ export class HooksService implements IHooksService { } } - return results; + // executeHooks flattens the per-directory results exactly once, so a hook + // returning several middlewares must contribute them individually or they + // stay nested one level too deep for decorateMethod's function filter. + return _.flatten(results); } private getCustomHooksByName(hookName: string): IHook[] { @@ -447,33 +569,44 @@ export class HooksService implements IHooksService { private shouldExecuteInProcess(scriptSource: string): boolean { try { - const esprima = require("esprima"); - const ast = esprima.parse(scriptSource); - - let inproc = false; - ast.body.forEach((statement: any) => { - if ( - statement.type !== "ExpressionStatement" || - statement.expression.type !== "AssignmentExpression" - ) { - return; + // required lazily so that CLI startup does not pay the cost of loading + // the TypeScript compiler, which is only needed when a hook runs. + const ts = require("typescript"); + const sourceFile = ts.createSourceFile( + "hook.js", + scriptSource, + ts.ScriptTarget.Latest, + /* setParentNodes */ false, + ts.ScriptKind.JS, + ); + + const isExportsTarget = (node: any): boolean => { + if (!ts.isPropertyAccessExpression(node)) { + return false; } - const left = statement.expression.left; - if ( - left.type === "MemberExpression" && - left.object && - left.object.type === "Identifier" && - left.object.name === "module" && - left.property && - left.property.type === "Identifier" && - left.property.name === "exports" - ) { - inproc = true; + if (!ts.isIdentifier(node.expression)) { + return false; } - }); - return inproc; + const object = node.expression.text; + const property = node.name.text; + + return ( + (object === "module" && property === "exports") || + (object === "exports" && property === "default") + ); + }; + + return sourceFile.statements.some((statement: any) => { + return ( + ts.isExpressionStatement(statement) && + ts.isBinaryExpression(statement.expression) && + statement.expression.operatorToken.kind === + ts.SyntaxKind.EqualsToken && + isExportsTarget(statement.expression.left) + ); + }); } catch (err) { return false; } diff --git a/lib/common/services/qr.ts b/lib/common/services/qr.ts deleted file mode 100644 index 3328d89e61..0000000000 --- a/lib/common/services/qr.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { imageSync } from "qr-image"; -import { escape } from "querystring"; -import { injector } from "../yok"; -import { IQrCodeGenerator } from "../declarations"; - -export class QrCodeGenerator implements IQrCodeGenerator { - constructor( - private $staticConfig: Config.IStaticConfig, - private $logger: ILogger - ) {} - - public async generateDataUri(data: string): Promise { - let result: string = null; - try { - const qrSvg = imageSync(data, { - size: this.$staticConfig.QR_SIZE, - type: "svg", - }).toString(); - result = `data:image/svg+xml;utf-8,${escape(qrSvg)}`; - } catch (err) { - this.$logger.trace(`Failed to generate QR code for ${data}`, err); - } - - return result; - } -} - -injector.register("qr", QrCodeGenerator); diff --git a/lib/common/test/definitions/mocha.d.ts b/lib/common/test/definitions/mocha.d.ts deleted file mode 100644 index 5c3a07ac15..0000000000 --- a/lib/common/test/definitions/mocha.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Type definitions for mocha 1.9.0 -// Project: http://visionmedia.github.io/mocha/ -// Definitions by: Kazi Manzur Rashid -// DefinitelyTyped: https://github.com/borisyankov/DefinitelyTyped - -interface Mocha { - // Setup mocha with the given setting options. - setup(options: MochaSetupOptions): Mocha; - - //Run tests and invoke `fn()` when complete. - run(callback?: () => void): void; - - // Set reporter as function - reporter(reporter: () => void): Mocha; - - // Set reporter, defaults to "dot" - reporter(reporter: string): Mocha; - - // Enable growl support. - growl(): Mocha; -} - -interface MochaSetupOptions { - //milliseconds to wait before considering a test slow - slow?: number; - - // timeout in milliseconds - timeout?: number; - - // ui name "bdd", "tdd", "exports" etc - ui?: string; - - //array of accepted globals - globals?: any[]; - - // reporter instance (function or string), defaults to `mocha.reporters.Dot` - reporter?: any; - - // bail on the first test failure - bail?: Boolean; - - // ignore global leaks - ignoreLeaks?: Boolean; - - // grep string or regexp to filter tests with - grep?: any; -} - -declare module mocha { - interface Done { - (error?: Error): void; - } -} - -declare var describe: { - (description: string, spec: () => void): void; - only(description: string, spec: () => void): void; - skip(description: string, spec: () => void): void; - timeout(ms: number): void; -}; - -declare var it: { - (expectation: string, assertion?: () => void): void; - (expectation: string, assertion?: (done: mocha.Done) => void): void; - only(expectation: string, assertion?: () => void): void; - only(expectation: string, assertion?: (done: mocha.Done) => void): void; - skip(expectation: string, assertion?: () => void): void; - skip(expectation: string, assertion?: (done: mocha.Done) => void): void; - timeout(ms: number): void; -}; - -declare function before(action: () => void): void; - -declare function before(action: (done: mocha.Done) => void): void; - -declare function after(action: () => void): void; - -declare function after(action: (done: mocha.Done) => void): void; - -declare function beforeEach(action: () => void): void; - -declare function beforeEach(action: (done: mocha.Done) => void): void; - -declare function afterEach(action: () => void): void; - -declare function afterEach(action: (done: mocha.Done) => void): void; diff --git a/lib/common/test/unit-tests/analytics-service.ts b/lib/common/test/unit-tests/analytics-service.ts index 000160cf3d..3dd3d5dfb9 100644 --- a/lib/common/test/unit-tests/analytics-service.ts +++ b/lib/common/test/unit-tests/analytics-service.ts @@ -128,7 +128,7 @@ describe("analytics-service", () => { service = null; }); - after(() => { + afterAll(() => { setIsInteractive(null); }); @@ -147,8 +147,8 @@ describe("analytics-service", () => { }); it("returns false when analytics status is disabled", async () => { - baseTestScenario.exceptionsTracking = baseTestScenario.featureTracking = - false; + baseTestScenario.exceptionsTracking = + baseTestScenario.featureTracking = false; const testInjector = createTestInjector(baseTestScenario); service = testInjector.resolve("analyticsService"); const staticConfig: Config.IStaticConfig = @@ -377,8 +377,8 @@ describe("analytics-service", () => { }); it("does nothing when exception and feature tracking are already set", async () => { - baseTestScenario.featureTracking = baseTestScenario.exceptionsTracking = - true; + baseTestScenario.featureTracking = + baseTestScenario.exceptionsTracking = true; const testInjector = createTestInjector(baseTestScenario); service = testInjector.resolve("analyticsService"); await service.checkConsent(); diff --git a/lib/common/test/unit-tests/appbuilder/device-emitter.ts b/lib/common/test/unit-tests/appbuilder/device-emitter.ts index ec4ec9bacc..33f43bc013 100644 --- a/lib/common/test/unit-tests/appbuilder/device-emitter.ts +++ b/lib/common/test/unit-tests/appbuilder/device-emitter.ts @@ -1,3 +1,4 @@ +import { withDone, DoneCallback } from "../../with-done"; import { Yok } from "../../../yok"; import { assert } from "chai"; import * as _ from "lodash"; @@ -63,7 +64,7 @@ describe("deviceEmitter", () => { describe(deviceEvent, () => { const attachDeviceEventVerificationHandler = ( expectedDeviceInfo: any, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on(deviceEvent, (deviceInfo: Mobile.IDeviceInfo) => { assert.deepStrictEqual(deviceInfo, expectedDeviceInfo); @@ -72,21 +73,24 @@ describe("deviceEmitter", () => { }); }; - it("is raised when working with device", (done: mocha.Done) => { - attachDeviceEventVerificationHandler( - deviceInstance.deviceInfo, - done - ); - devicesService.emit(deviceEvent, deviceInstance); - }); + it( + "is raised when working with device", + withDone((done) => { + attachDeviceEventVerificationHandler( + deviceInstance.deviceInfo, + done, + ); + devicesService.emit(deviceEvent, deviceInstance); + }), + ); }); - } + }, ); describe("openDeviceLogStream", () => { const attachDeviceEventVerificationHandler = ( expectedDeviceInfo: any, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on( DeviceDiscoveryEventNames.DEVICE_FOUND, @@ -97,21 +101,24 @@ describe("deviceEmitter", () => { setTimeout(() => { assert.isTrue( isOpenDeviceLogStreamCalled, - "When device is found, openDeviceLogStream must be called immediately." + "When device is found, openDeviceLogStream must be called immediately.", ); done(); }, 0); - } + }, ); }; - it("is called when working with device", (done: mocha.Done) => { - attachDeviceEventVerificationHandler(deviceInstance.deviceInfo, done); - devicesService.emit( - DeviceDiscoveryEventNames.DEVICE_FOUND, - deviceInstance - ); - }); + it( + "is called when working with device", + withDone((done) => { + attachDeviceEventVerificationHandler(deviceInstance.deviceInfo, done); + devicesService.emit( + DeviceDiscoveryEventNames.DEVICE_FOUND, + deviceInstance, + ); + }), + ); }); describe("deviceLogProvider on data", () => { @@ -126,7 +133,7 @@ describe("deviceEmitter", () => { const attachDeviceLogDataVerificationHandler = ( expectedDeviceIdentifier: string, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on( DEVICE_LOG_EVENT_NAME, @@ -135,25 +142,28 @@ describe("deviceEmitter", () => { assert.deepStrictEqual(data, expectedDeviceLogData); // Wait for all operations to be completed and call done after that. setTimeout(() => done(), 0); - } + }, ); }; - it("is called when device reports data", (done: mocha.Done) => { - attachDeviceLogDataVerificationHandler( - deviceInstance.deviceInfo.identifier, - done - ); - devicesService.emit( - DeviceDiscoveryEventNames.DEVICE_FOUND, - deviceInstance - ); - deviceLogProvider.emit( - "data", - deviceInstance.deviceInfo.identifier, - expectedDeviceLogData - ); - }); + it( + "is called when device reports data", + withDone((done) => { + attachDeviceLogDataVerificationHandler( + deviceInstance.deviceInfo.identifier, + done, + ); + devicesService.emit( + DeviceDiscoveryEventNames.DEVICE_FOUND, + deviceInstance, + ); + deviceLogProvider.emit( + "data", + deviceInstance.deviceInfo.identifier, + expectedDeviceLogData, + ); + }), + ); }); }); @@ -165,42 +175,45 @@ describe("deviceEmitter", () => { const attachApplicationEventVerificationHandler = ( expectedDeviceIdentifier: string, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on( applicationEvent, (deviceIdentifier: string, appIdentifier: string) => { assert.deepStrictEqual( deviceIdentifier, - expectedDeviceIdentifier + expectedDeviceIdentifier, ); assert.deepStrictEqual( appIdentifier, - expectedApplicationIdentifier + expectedApplicationIdentifier, ); // Wait for all operations to be completed and call done after that. setTimeout(() => done(), 0); - } + }, ); }; - it("is raised when working with device", (done: mocha.Done) => { - attachApplicationEventVerificationHandler( - deviceInstance.deviceInfo.identifier, - done - ); - devicesService.emit( - DeviceDiscoveryEventNames.DEVICE_FOUND, - deviceInstance - ); - deviceInstance.applicationManager.emit( - applicationEvent, - expectedApplicationIdentifier - ); - }); + it( + "is raised when working with device", + withDone((done) => { + attachApplicationEventVerificationHandler( + deviceInstance.deviceInfo.identifier, + done, + ); + devicesService.emit( + DeviceDiscoveryEventNames.DEVICE_FOUND, + deviceInstance, + ); + deviceInstance.applicationManager.emit( + applicationEvent, + expectedApplicationIdentifier, + ); + }), + ); }); - } + }, ); _.each( @@ -209,41 +222,44 @@ describe("deviceEmitter", () => { describe(applicationEvent, () => { const attachDebuggableEventVerificationHandler = ( expectedDebuggableAppInfo: Mobile.IDeviceApplicationInformation, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on( applicationEvent, (debuggableAppInfo: Mobile.IDeviceApplicationInformation) => { assert.deepStrictEqual( debuggableAppInfo, - expectedDebuggableAppInfo + expectedDebuggableAppInfo, ); // Wait for all operations to be completed and call done after that. setTimeout(() => done(), 0); - } + }, ); }; - it("is raised when working with device", (done: mocha.Done) => { - const debuggableAppInfo: Mobile.IDeviceApplicationInformation = { - appIdentifier: "app identifier", - deviceIdentifier: deviceInstance.deviceInfo.identifier, - framework: "cordova", - }; - - attachDebuggableEventVerificationHandler(debuggableAppInfo, done); - devicesService.emit( - DeviceDiscoveryEventNames.DEVICE_FOUND, - deviceInstance - ); - deviceInstance.applicationManager.emit( - applicationEvent, - debuggableAppInfo - ); - }); + it( + "is raised when working with device", + withDone((done) => { + const debuggableAppInfo: Mobile.IDeviceApplicationInformation = { + appIdentifier: "app identifier", + deviceIdentifier: deviceInstance.deviceInfo.identifier, + framework: "cordova", + }; + + attachDebuggableEventVerificationHandler(debuggableAppInfo, done); + devicesService.emit( + DeviceDiscoveryEventNames.DEVICE_FOUND, + deviceInstance, + ); + deviceInstance.applicationManager.emit( + applicationEvent, + debuggableAppInfo, + ); + }), + ); }); - } + }, ); _.each( @@ -268,56 +284,58 @@ describe("deviceEmitter", () => { expectedDeviceIdentifier: string, expectedAppIdentifier: string, expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo, - done: mocha.Done + done: DoneCallback, ) => { deviceEmitter.on( applicationEvent, ( deviceIdentifier: string, appIdentifier: string, - debuggableViewInfo: Mobile.IDebugWebViewInfo + debuggableViewInfo: Mobile.IDebugWebViewInfo, ) => { assert.deepStrictEqual( deviceIdentifier, - expectedDeviceIdentifier + expectedDeviceIdentifier, ); assert.deepStrictEqual(appIdentifier, expectedAppIdentifier); assert.deepStrictEqual( debuggableViewInfo, - expectedDebuggableViewInfo + expectedDebuggableViewInfo, ); // Wait for all operations to be completed and call done after that. setTimeout(done, 0); - } + }, ); }; - it("is raised when working with device", (done: mocha.Done) => { - const expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo = createDebuggableWebView( - "test1" - ); - - attachDebuggableEventVerificationHandler( - deviceInstance.deviceInfo.identifier, - appId, - expectedDebuggableViewInfo, - done - ); - devicesService.emit( - DeviceDiscoveryEventNames.DEVICE_FOUND, - deviceInstance - ); - deviceInstance.applicationManager.emit( - applicationEvent, - appId, - expectedDebuggableViewInfo - ); - }); + it( + "is raised when working with device", + withDone((done) => { + const expectedDebuggableViewInfo: Mobile.IDebugWebViewInfo = + createDebuggableWebView("test1"); + + attachDebuggableEventVerificationHandler( + deviceInstance.deviceInfo.identifier, + appId, + expectedDebuggableViewInfo, + done, + ); + devicesService.emit( + DeviceDiscoveryEventNames.DEVICE_FOUND, + deviceInstance, + ); + deviceInstance.applicationManager.emit( + applicationEvent, + appId, + expectedDebuggableViewInfo, + ); + }), + ); }); - } + }, ); }); }); diff --git a/lib/common/test/unit-tests/decorators.ts b/lib/common/test/unit-tests/decorators.ts index 95db6c4171..702211364c 100644 --- a/lib/common/test/unit-tests/decorators.ts +++ b/lib/common/test/unit-tests/decorators.ts @@ -29,7 +29,7 @@ describe("decorators", () => { injector.register("performanceService", stubs.PerformanceService); }); - after(() => { + afterAll(() => { // Make sure global injector is clean for next tests that will be executed. setGlobalInjector(new Yok()); }); @@ -38,7 +38,7 @@ describe("decorators", () => { const generatePublicApiFromExportedDecorator = () => { assert.deepStrictEqual( injector.publicApi.__modules__[moduleName], - undefined + undefined, ); const resultFunction: any = decoratorsLib.exported(moduleName); // Call this line in order to generate publicApi and get the real result @@ -56,7 +56,7 @@ describe("decorators", () => { const actualResult = exportedFunctionResult( {}, "myTest1", - expectedResult + expectedResult, ); assert.deepStrictEqual(actualResult, expectedResult); }); @@ -67,9 +67,8 @@ describe("decorators", () => { }`, () => { injector.register(moduleName, { propertyName: () => expectedResult }); generatePublicApiFromExportedDecorator(); - const actualResult: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](); + const actualResult: any = + injector.publicApi.__modules__[moduleName][propertyName](); assert.deepStrictEqual(actualResult, expectedResult); }); @@ -78,86 +77,71 @@ describe("decorators", () => { }`, () => { injector.register(moduleName, { propertyName: (arg: any) => arg }); generatePublicApiFromExportedDecorator(); - const actualResult: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](expectedResult); + const actualResult: any = + injector.publicApi.__modules__[moduleName][propertyName]( + expectedResult, + ); assert.deepStrictEqual(actualResult, expectedResult); }); }); - it("returns Promise, which is resolved to correct value (function without arguments)", (done: mocha.Done) => { + it("returns Promise, which is resolved to correct value (function without arguments)", async () => { const expectedResult = "result"; injector.register(moduleName, { propertyName: async () => expectedResult, }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](); - promise - .then((val: string) => { - assert.deepStrictEqual(val, expectedResult); - }) - .then(done) - .catch(done); + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](); + await promise.then((val: string) => { + assert.deepStrictEqual(val, expectedResult); + }); }); - it("returns Promise, which is resolved to correct value (function with arguments)", (done: mocha.Done) => { + it("returns Promise, which is resolved to correct value (function with arguments)", async () => { const expectedArgs = ["result", "result1", "result2"]; injector.register(moduleName, { propertyName: async (functionArgs: string[]) => functionArgs, }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](expectedArgs); - promise - .then((val: string[]) => { - assert.deepStrictEqual(val, expectedArgs); - }) - .then(done) - .catch(done); + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](expectedArgs); + await promise.then((val: string[]) => { + assert.deepStrictEqual(val, expectedArgs); + }); }); - it("returns Promise, which is resolved to correct value (function returning Promise without arguments)", (done: mocha.Done) => { + it("returns Promise, which is resolved to correct value (function returning Promise without arguments)", async () => { const expectedResult = "result"; injector.register(moduleName, { propertyName: async () => expectedResult, }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](); - promise - .then((val: string) => { - assert.deepStrictEqual(val, expectedResult); - }) - .then(done) - .catch(done); + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](); + await promise.then((val: string) => { + assert.deepStrictEqual(val, expectedResult); + }); }); - it("returns Promise, which is resolved to correct value (function returning Promise with arguments)", (done: mocha.Done) => { + it("returns Promise, which is resolved to correct value (function returning Promise with arguments)", async () => { const expectedArgs = ["result", "result1", "result2"]; injector.register(moduleName, { propertyName: async (args: string[]) => args, }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](expectedArgs); - promise - .then((val: string[]) => { - assert.deepStrictEqual(val, expectedArgs); - }) - .then(done) - .catch(done); + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](expectedArgs); + await promise.then((val: string[]) => { + assert.deepStrictEqual(val, expectedArgs); + }); }); - it("rejects Promise, which is resolved to correct error (function without arguments throws)", (done: mocha.Done) => { + it("rejects Promise, which is resolved to correct error (function without arguments throws)", async () => { const expectedError = new Error("Test msg"); injector.register(moduleName, { propertyName: async () => { @@ -166,25 +150,21 @@ describe("decorators", () => { }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](); - promise - .then( - (result: any) => { - throw new Error( - "Then method MUST not be called when promise is rejected!" - ); - }, - (err: Error) => { - assert.deepStrictEqual(err, expectedError); - } - ) - .then(done) - .catch(done); - }); - - it("rejects Promise, which is resolved to correct error (function returning Promise without arguments throws)", (done: mocha.Done) => { + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](); + await promise.then( + (result: any) => { + throw new Error( + "Then method MUST not be called when promise is rejected!", + ); + }, + (err: Error) => { + assert.deepStrictEqual(err, expectedError); + }, + ); + }); + + it("rejects Promise, which is resolved to correct error (function returning Promise without arguments throws)", async () => { const expectedError = new Error("Test msg"); injector.register(moduleName, { propertyName: async () => { @@ -193,25 +173,21 @@ describe("decorators", () => { }); generatePublicApiFromExportedDecorator(); - const promise: any = injector.publicApi.__modules__[moduleName][ - propertyName - ](); - promise - .then( - (result: any) => { - throw new Error( - "Then method MUST not be called when promise is rejected!" - ); - }, - (err: Error) => { - assert.deepStrictEqual(err.message, expectedError.message); - } - ) - .then(done) - .catch(done); - }); - - it("returns Promises, which are resolved to correct value (function returning Promise[] without arguments)", (done: mocha.Done) => { + const promise: any = + injector.publicApi.__modules__[moduleName][propertyName](); + await promise.then( + (result: any) => { + throw new Error( + "Then method MUST not be called when promise is rejected!", + ); + }, + (err: Error) => { + assert.deepStrictEqual(err.message, expectedError.message); + }, + ); + }); + + it("returns Promises, which are resolved to correct value (function returning Promise[] without arguments)", async () => { const expectedResultsArr = ["result1", "result2", "result3"]; injector.register(moduleName, { propertyName: () => @@ -219,20 +195,16 @@ describe("decorators", () => { }); generatePublicApiFromExportedDecorator(); - const promises: Promise[] = injector.publicApi.__modules__[ - moduleName - ][propertyName](); - Promise.all(promises) - .then((promiseResults: string[]) => { - _.each(promiseResults, (val: string, index: number) => { - assert.deepStrictEqual(val, expectedResultsArr[index]); - }); - }) - .then(() => done()) - .catch(done); + const promises: Promise[] = + injector.publicApi.__modules__[moduleName][propertyName](); + await Promise.all(promises).then((promiseResults: string[]) => { + _.each(promiseResults, (val: string, index: number) => { + assert.deepStrictEqual(val, expectedResultsArr[index]); + }); + }); }); - it("rejects Promises, which are resolved to correct error (function returning Promise[] without arguments throws)", (done: mocha.Done) => { + it("rejects Promises, which are resolved to correct error (function returning Promise[] without arguments throws)", async () => { const expectedErrors = [ new Error("result1"), new Error("result2"), @@ -246,40 +218,37 @@ describe("decorators", () => { }); generatePublicApiFromExportedDecorator(); - new Promise((onFulfilled: Function, onRejected: Function) => { - const promises: Promise[] = injector.publicApi.__modules__[ - moduleName - ][propertyName](); + await new Promise((onFulfilled: Function, onRejected: Function) => { + const promises: Promise[] = + injector.publicApi.__modules__[moduleName][propertyName](); _.each(promises, (promise, index) => promise.then( (result: any) => { onRejected( new Error( - `Then method MUST not be called when promise is rejected!. Result of promise is: ${result}` - ) + `Then method MUST not be called when promise is rejected!. Result of promise is: ${result}`, + ), ); }, (err: Error) => { if (err.message !== expectedErrors[index].message) { onRejected( new Error( - `Error message of rejected promise is not the expected one: expected: "${expectedErrors[index].message}", but was: "${err.message}".` - ) + `Error message of rejected promise is not the expected one: expected: "${expectedErrors[index].message}", but was: "${err.message}".`, + ), ); } if (index + 1 === expectedErrors.length) { onFulfilled(); } - } - ) + }, + ), ); - }) - .then(done) - .catch(done); + }); }); - it("rejects only Promises which throw, resolves the others correctly (function returning Promise[] without arguments)", (done: mocha.Done) => { + it("rejects only Promises which throw, resolves the others correctly (function returning Promise[] without arguments)", async () => { const expectedResultsArr: any[] = ["result1", new Error("result2")]; injector.register(moduleName, { propertyName: () => @@ -287,10 +256,9 @@ describe("decorators", () => { }); generatePublicApiFromExportedDecorator(); - new Promise((onFulfilled: Function, onRejected: Function) => { - const promises: Promise[] = injector.publicApi.__modules__[ - moduleName - ][propertyName](); + await new Promise((onFulfilled: Function, onRejected: Function) => { + const promises: Promise[] = + injector.publicApi.__modules__[moduleName][propertyName](); _.each(promises, (promise, index) => promise.then( (val: string) => { @@ -302,17 +270,15 @@ describe("decorators", () => { (err: Error) => { assert.deepStrictEqual( err.message, - expectedResultsArr[index].message + expectedResultsArr[index].message, ); if (index + 1 === expectedResultsArr.length) { onFulfilled(); } - } - ) + }, + ), ); - }) - .then(done) - .catch(done); + }); }); it("when function throws, raises the error only when the public API is called, not when decorator is applied", () => { @@ -325,7 +291,7 @@ describe("decorators", () => { generatePublicApiFromExportedDecorator(); assert.throws( () => injector.publicApi.__modules__[moduleName][propertyName](), - errorMessage + errorMessage, ); }); }); @@ -344,7 +310,7 @@ describe("decorators", () => { const declaredMethod = decoratorsLib.cache()( {}, propertyName, - descriptor + descriptor, ); const expectedResult = 5; const actualResult = declaredMethod.value(expectedResult); @@ -363,7 +329,7 @@ describe("decorators", () => { const expectedResultForInstance1 = 1; assert.deepStrictEqual( instance1.method(expectedResultForInstance1), - expectedResultForInstance1 + expectedResultForInstance1, ); // the first call should give us the expected result. all consecutive calls must return the same result. _.range(10).forEach((iteration) => { @@ -378,7 +344,7 @@ describe("decorators", () => { assert.deepStrictEqual( instance2.method(expectedResultForInstance2), expectedResultForInstance2, - "Instance 2 should return new result." + "Instance 2 should return new result.", ); // the first call should give us the expected result. all consecutive calls must return the same result. _.range(10).forEach((iteration) => { @@ -394,14 +360,14 @@ describe("decorators", () => { const expectedResultForInstance1 = 1; assert.deepStrictEqual( await instance1.promisifiedMethod(expectedResultForInstance1), - expectedResultForInstance1 + expectedResultForInstance1, ); // the first call should give us the expected result. all consecutive calls must return the same result. for (let iteration = 0; iteration < 10; iteration++) { const promise = instance1.promisifiedMethod(iteration); assert.isTrue( isPromise(promise), - "Returned result from the decorator should be promise." + "Returned result from the decorator should be promise.", ); const currentResult = await promise; assert.deepStrictEqual(currentResult, expectedResultForInstance1); @@ -433,7 +399,7 @@ describe("decorators", () => { const expectedResult = 1; assert.deepStrictEqual( await instance[methodName](expectedResult), - expectedResult + expectedResult, ); assert.isTrue(instance.isInvokeBeforeMethodCalled); }; @@ -454,7 +420,7 @@ describe("decorators", () => { const expectedResult = 1; assert.deepStrictEqual( await instance[methodName](expectedResult), - expectedResult + expectedResult, ); assert.isTrue(instance.isInvokeBeforeMethodCalled); @@ -464,7 +430,7 @@ describe("decorators", () => { instance.isInvokeBeforeMethodCalled = false; assert.deepStrictEqual( await instance[methodName](iteration), - iteration + iteration, ); assert.isTrue(instance.isInvokeBeforeMethodCalled); assert.deepStrictEqual(instance.invokedBeforeCount, iteration + 1); @@ -487,7 +453,7 @@ describe("decorators", () => { const expectedResult = 1; await assert.isRejected( instance[methodName](expectedResult), - expectedResult + expectedResult, ); assert.isTrue(instance.isInvokeBeforeMethodCalled); }; @@ -508,7 +474,7 @@ describe("decorators", () => { const expectedResult = 1; assert.deepStrictEqual( await instance[methodName](expectedResult), - expectedResult + expectedResult, ); assert.isTrue(instance.isInvokeBeforeMethodCalled); assert.deepStrictEqual(instance.invokedBeforeArgument, "arg1"); @@ -615,14 +581,14 @@ describe("decorators", () => { it("method has same toString", () => { assert.equal( testInstance.testMethod.toString(), - undecoratedTestInstance.testMethod.toString() + undecoratedTestInstance.testMethod.toString(), ); }); it("method has same name", () => { assert.equal( testInstance.testMethod.name, - undecoratedTestInstance.testMethod.name + undecoratedTestInstance.testMethod.name, ); }); @@ -635,7 +601,7 @@ describe("decorators", () => { const performanceService = testInjector.resolve("performanceService"); const processExecutionDataStub: sinon.SinonStub = sinon.stub( performanceService, - "processExecutionData" + "processExecutionData", ); const checkSubCall = (call: sinon.SinonSpyCall, methodData: string) => { @@ -657,7 +623,7 @@ describe("decorators", () => { checkSubCall(processExecutionDataStub.firstCall, "TestClass__testMethod"); checkSubCall( processExecutionDataStub.secondCall, - "TestClass__testAsyncMehtod" + "TestClass__testAsyncMehtod", ); }); }); @@ -733,7 +699,7 @@ describe("decorators", () => { assert.equal(warnings.length, 1); assert.equal( warnings[0], - `depMethodWithoutParam is deprecated. ${testDepMessage}` + `depMethodWithoutParam is deprecated. ${testDepMessage}`, ); }); @@ -744,7 +710,7 @@ describe("decorators", () => { assert.equal(warnings.length, 1); assert.equal( warnings[0], - `depMethodWithParam is deprecated. ${testDepMessage}` + `depMethodWithParam is deprecated. ${testDepMessage}`, ); }); @@ -755,7 +721,7 @@ describe("decorators", () => { assert.equal(warnings.length, 1); assert.equal( warnings[0], - `depAsyncMethod is deprecated. ${testDepMessage}` + `depAsyncMethod is deprecated. ${testDepMessage}`, ); }); @@ -792,7 +758,7 @@ describe("decorators", () => { assert.equal(warnings.length, 1); assert.equal( warnings[0], - `TestClassDeprecated is deprecated. ${testDepMessage}` + `TestClassDeprecated is deprecated. ${testDepMessage}`, ); }); }); diff --git a/lib/common/test/unit-tests/errors.ts b/lib/common/test/unit-tests/errors.ts index 0d9a1fb7f2..7988be57e1 100644 --- a/lib/common/test/unit-tests/errors.ts +++ b/lib/common/test/unit-tests/errors.ts @@ -15,12 +15,12 @@ describe("errors", () => { let isInteractive = false; let processExitCode = 0; - before(() => { + beforeAll(() => { // @ts-expect-error helpers.isInteractive = () => isInteractive; }); - after(() => { + afterAll(() => { // @ts-expect-error helpers.isInteractive = originalIsInteractive; }); @@ -139,7 +139,7 @@ describe("errors", () => { "The error output must contain the error message", ); assert.isTrue( - logger.errorOutput.indexOf("at next") !== -1, + /\n\s+at\s/.test(logger.errorOutput), "The error output must contain callstack", ); assert.isTrue( diff --git a/lib/common/test/unit-tests/helpers.ts b/lib/common/test/unit-tests/helpers.ts index 5aeea6f107..39db1dbdf2 100644 --- a/lib/common/test/unit-tests/helpers.ts +++ b/lib/common/test/unit-tests/helpers.ts @@ -25,7 +25,7 @@ describe("helpers", () => { assert.deepStrictEqual( actualResult, testData.expectedResult, - `For input ${testData.input}, the expected result is: ${testData.expectedResult}, but actual result is: ${actualResult}.` + `For input ${testData.input}, the expected result is: ${testData.expectedResult}, but actual result is: ${actualResult}.`, ); }; @@ -82,9 +82,9 @@ describe("helpers", () => { assert.deepStrictEqual( helpers.appendZeroesToVersion( testCase.input, - testCase.requiredVersionLength + testCase.requiredVersionLength, ), - testCase.expectedResult + testCase.expectedResult, ); }); }); @@ -97,13 +97,13 @@ describe("helpers", () => { initialDataValues: any[], handledElements: any[], element: any, - passedChunkSize: number + passedChunkSize: number, ) => { return new Promise((resolve) => setImmediate(() => { const remainingElements = _.difference( initialDataValues, - handledElements + handledElements, ); const isFromLastChunk = element + passedChunkSize > initialDataValues.length; @@ -117,17 +117,17 @@ describe("helpers", () => { Math.floor(indexOfElement / passedChunkSize) + 1; const expectedRemainingElements = _.drop( initialDataValues, - chunkNumber * passedChunkSize + chunkNumber * passedChunkSize, ); assert.deepStrictEqual( remainingElements, - expectedRemainingElements + expectedRemainingElements, ); } resolve(); - }) + }), ); }; @@ -146,9 +146,9 @@ describe("helpers", () => { initialData, handledElements, element, - chunkSize + chunkSize, ); - } + }, ); }); @@ -174,9 +174,9 @@ describe("helpers", () => { initialDataValues, handledElements, element, - chunkSize + chunkSize, ); - } + }, ); }); }); @@ -312,13 +312,13 @@ describe("helpers", () => { // The tests will use strings in order to skip transpilation of lambdas to functions. it("returns correct property name for ES5 functions", () => { _.each(ES5Functions, (testData) => - assertTestData(testData, helpers.getPropertyName) + assertTestData(testData, helpers.getPropertyName), ); }); it("returns correct property name for ES6 functions", () => { _.each(ES6Functions, (testData) => - assertTestData(testData, helpers.getPropertyName) + assertTestData(testData, helpers.getPropertyName), ); }); }); @@ -389,7 +389,7 @@ describe("helpers", () => { it("returns expected result", () => { _.each(toBooleanTestData, (testData) => - assertTestData(testData, helpers.toBoolean) + assertTestData(testData, helpers.toBoolean), ); }); @@ -461,7 +461,7 @@ describe("helpers", () => { it("returns expected result", () => { _.each(isNullOrWhitespaceTestData, (t) => - assertTestData(t, helpers.isNullOrWhitespace) + assertTestData(t, helpers.isNullOrWhitespace), ); }); @@ -539,21 +539,19 @@ describe("helpers", () => { ]; _.each(settlePromisesTestData, (testData, inputNumber) => { - it(`returns correct data, test case ${inputNumber}`, (done: any) => { - helpers + it(`returns correct data, test case ${inputNumber}`, async () => { + await helpers .settlePromises(testData.input) .then((res) => { assert.deepStrictEqual(res, testData.expectedResult); }) .catch((err) => { assert.deepStrictEqual(err.message, testData.expectedError); - }) - .then(done) - .catch(done); + }); }); }); - it("executes all promises even when some of them are rejected", (done: mocha.Done) => { + it("executes all promises even when some of them are rejected", async () => { let isPromiseSettled = false; const testData: ITestData = { @@ -565,24 +563,19 @@ describe("helpers", () => { expectedError: getErrorMessage([1]), }; - helpers - .settlePromises(testData.input) - .then( - (res) => { - assert.deepStrictEqual(res, testData.expectedResult); - }, - (err) => { - assert.deepStrictEqual(err.message, testData.expectedError); - } - ) - .then(() => { - assert.isTrue( - isPromiseSettled, - "When the first promise is rejected, the second one should still be executed." - ); - done(); - }) - .catch(done); + await helpers.settlePromises(testData.input).then( + (res) => { + assert.deepStrictEqual(res, testData.expectedResult); + }, + (err) => { + assert.deepStrictEqual(err.message, testData.expectedError); + }, + ); + + assert.isTrue( + isPromiseSettled, + "When the first promise is rejected, the second one should still be executed.", + ); }); }); @@ -598,12 +591,12 @@ describe("helpers", () => { const assertPidTestData = (testData: IiOSSimulatorPidTestData) => { const actualResult = helpers.getPidFromiOSSimulatorLogs( testData.appId || appId, - testData.input + testData.input, ); assert.deepStrictEqual( actualResult, testData.expectedResult, - `For input ${testData.input}, the expected result is: ${testData.expectedResult}, but actual result is: ${actualResult}.` + `For input ${testData.input}, the expected result is: ${testData.expectedResult}, but actual result is: ${actualResult}.`, ); }; @@ -670,7 +663,7 @@ describe("helpers", () => { it("returns expected result", () => { _.each(getPidFromiOSSimulatorLogsTestData, (testData) => - assertPidTestData(testData) + assertPidTestData(testData), ); }); }); @@ -750,28 +743,28 @@ describe("helpers", () => { ]; const assertValueFromNestedObjectTestData = ( - testData: IValueFromNestedObjectTestData + testData: IValueFromNestedObjectTestData, ) => { const actualResult = helpers.getValueFromNestedObject( testData.input, - testData.key + testData.key, ); assert.deepStrictEqual( actualResult, testData.expectedResult, `For input ${JSON.stringify( - testData.input + testData.input, )}, the expected result is: ${JSON.stringify( - testData.expectedResult || "undefined" + testData.expectedResult || "undefined", )}, but actual result is: ${JSON.stringify( - actualResult || "undefined" - )}.` + actualResult || "undefined", + )}.`, ); }; it("returns expected result", () => { _.each(getValueFromNestedObjectTestData, (testData) => - assertValueFromNestedObjectTestData(testData) + assertValueFromNestedObjectTestData(testData), ); }); }); @@ -816,7 +809,7 @@ describe("helpers", () => { _.each(testData, (testCase) => { assert.deepStrictEqual( helpers.isNumberWithoutExponent(testCase.input), - testCase.expectedResult + testCase.expectedResult, ); }); }); @@ -845,14 +838,12 @@ describe("helpers", () => { expectedOutput: false, }, { - name: - "returns false when neither -g/--global are passed on terminal, but similar flag is passed", + name: "returns false when neither -g/--global are passed on terminal, but similar flag is passed", input: ["install", "nativescript", "--globalEnv"], expectedOutput: false, }, { - name: - "returns false when neither -g/--global are passed on terminal, but trying to install global package", + name: "returns false when neither -g/--global are passed on terminal, but trying to install global package", input: ["install", "global"], expectedOutput: false, }, @@ -1043,4 +1034,81 @@ const test = require("./test");`, assert.isTrue(helpers.isInteractive()); }); }); + + describe("decorateMethod", () => { + const decorate = (middlewares: Function[], sink: Function): Function => { + const descriptor: TypedPropertyDescriptor = { value: sink }; + helpers.decorateMethod( + async () => middlewares, + async (method, self, result) => result, + )(null, "method", descriptor); + return descriptor.value; + }; + + it("passes the original args and the sink to a single middleware", async () => { + let received: any[]; + const middleware = async (args: any[], next: Function) => { + received = args; + return next.apply(null, ["replaced"]); + }; + const decorated = decorate([middleware], async function (...args: any[]) { + return args; + }); + + const result = await decorated.call({}, "original"); + + assert.deepStrictEqual(received, ["original"]); + assert.deepStrictEqual(result, ["replaced"]); + }); + + it("forwards args changed by every middleware, not only the innermost", async () => { + const seen: { [key: string]: any[] } = {}; + // The last middleware in the array is the outermost link, so it runs + // first. + const runsSecond = async (args: any[], next: Function) => { + seen.second = args.slice(); + return next.apply(null, args.concat("second")); + }; + const runsFirst = async (args: any[], next: Function) => { + seen.first = args.slice(); + return next.apply(null, args.concat("first")); + }; + const decorated = decorate( + [runsSecond, runsFirst], + async function (...args: any[]) { + seen.sink = args; + return "done"; + }, + ); + + const result = await decorated.call({}, "start"); + + assert.deepStrictEqual(seen.first, ["start"]); + assert.deepStrictEqual(seen.second, ["start", "first"]); + assert.deepStrictEqual(seen.sink, ["start", "first", "second"]); + assert.strictEqual(result, "done"); + }); + + it("keeps the current args when a middleware calls next() without arguments", async () => { + const seen: { [key: string]: any[] } = {}; + const inner = async (args: any[], next: Function) => { + seen.inner = args.slice(); + return next.apply(null, args); + }; + const outer = async (args: any[], next: Function) => { + return next(); + }; + const decorated = decorate( + [inner, outer], + async function (...args: any[]) { + seen.sink = args; + }, + ); + + await decorated.call({}, "kept"); + + assert.deepStrictEqual(seen.inner, ["kept"]); + assert.deepStrictEqual(seen.sink, ["kept"]); + }); + }); }); diff --git a/lib/common/test/unit-tests/mobile/android-emulator-service.ts b/lib/common/test/unit-tests/mobile/android-emulator-service.ts index 379701c8fc..e34f8283a4 100644 --- a/lib/common/test/unit-tests/mobile/android-emulator-service.ts +++ b/lib/common/test/unit-tests/mobile/android-emulator-service.ts @@ -23,6 +23,9 @@ function createTestInjector() { testInjector.register("utils", { getMilliSecondsTimeout: () => ({}), }); + testInjector.register("userSettingsService", { + getSettingValue: () => Promise.resolve(null), + }); return testInjector; } @@ -70,14 +73,14 @@ describe("androidEmulatorService", () => { const testInjector = createTestInjector(); androidEmulatorServices = testInjector.resolve("androidEmulatorServices"); androidVirtualDeviceService = testInjector.resolve( - "androidVirtualDeviceService" + "androidVirtualDeviceService", ); androidGenymotionService = testInjector.resolve("androidGenymotionService"); }); function mockGetEmulatorImages( avds: Mobile.IEmulatorImagesOutput, - genies: Mobile.IEmulatorImagesOutput + genies: Mobile.IEmulatorImagesOutput, ) { androidVirtualDeviceService.getEmulatorImages = () => Promise.resolve(avds); androidGenymotionService.getEmulatorImages = () => Promise.resolve(genies); @@ -94,7 +97,7 @@ describe("androidEmulatorService", () => { it("should return [] when there are no emulators are available", async () => { mockGetEmulatorImages( { devices: [], errors: [] }, - { devices: [], errors: [] } + { devices: [], errors: [] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, []); @@ -103,7 +106,7 @@ describe("androidEmulatorService", () => { it("should return avd emulators when only avd emulators are available", async () => { mockGetEmulatorImages( { devices: [avdEmulator], errors: [] }, - { devices: [], errors: [] } + { devices: [], errors: [] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, [avdEmulator]); @@ -112,7 +115,7 @@ describe("androidEmulatorService", () => { it("should return geny emulators when only geny emulators are available", async () => { mockGetEmulatorImages( { devices: [], errors: [] }, - { devices: [genyEmulator], errors: [] } + { devices: [genyEmulator], errors: [] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, [genyEmulator]); @@ -121,19 +124,19 @@ describe("androidEmulatorService", () => { it("should return avd and geny emulators when avd and geny emulators are available", async () => { mockGetEmulatorImages( { devices: [avdEmulator], errors: [] }, - { devices: [genyEmulator], errors: [] } + { devices: [genyEmulator], errors: [] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual( output.devices, - [avdEmulator].concat([genyEmulator]) + [avdEmulator].concat([genyEmulator]), ); assert.deepStrictEqual(output.errors, []); }); it("should return an error when avd error is thrown", async () => { mockGetEmulatorImages( { devices: [], errors: [mockError] }, - { devices: [], errors: [] } + { devices: [], errors: [] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, []); @@ -142,7 +145,7 @@ describe("androidEmulatorService", () => { it("should return an error when geny error is thrown", async () => { mockGetEmulatorImages( { devices: [], errors: [] }, - { devices: [], errors: [mockError] } + { devices: [], errors: [mockError] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, []); @@ -151,7 +154,7 @@ describe("androidEmulatorService", () => { it("should return an error when avd and geny errors are thrown", async () => { mockGetEmulatorImages( { devices: [], errors: [mockError] }, - { devices: [], errors: [mockError] } + { devices: [], errors: [mockError] }, ); const output = await androidEmulatorServices.getEmulatorImages(); assert.deepStrictEqual(output.devices, []); @@ -192,30 +195,26 @@ describe("androidEmulatorService", () => { it("should return null when no emulators are available", async () => { mockGetRunningEmulatorName({}); - const emulatorName = await androidEmulatorServices.getRunningEmulatorName( - "" - ); + const emulatorName = + await androidEmulatorServices.getRunningEmulatorName(""); assert.deepStrictEqual(emulatorName, undefined); }); it("should return null when there are available emulators but the provided emulatorId is not found", async () => { mockGetRunningEmulatorName({}); - const emulatorName = await androidEmulatorServices.getRunningEmulatorName( - "my emulator Id" - ); + const emulatorName = + await androidEmulatorServices.getRunningEmulatorName("my emulator Id"); assert.deepStrictEqual(emulatorName, undefined); }); it("should return avd emulator when the provided emulatorId is found", async () => { mockGetRunningEmulatorName({ avd: avdEmulatorName }); - const emulatorName = await androidEmulatorServices.getRunningEmulatorName( - avdEmulatorName - ); + const emulatorName = + await androidEmulatorServices.getRunningEmulatorName(avdEmulatorName); assert.deepStrictEqual(emulatorName, avdEmulatorName); }); it("should return geny emulator when the provided emulatorId is found", async () => { mockGetRunningEmulatorName({ geny: genyEmulatorName }); - const emulatorName = await androidEmulatorServices.getRunningEmulatorName( - genyEmulatorName - ); + const emulatorName = + await androidEmulatorServices.getRunningEmulatorName(genyEmulatorName); assert.deepStrictEqual(emulatorName, genyEmulatorName); }); it("should return avd emulator when there are avd and geny emulators", async () => { @@ -223,16 +222,15 @@ describe("androidEmulatorService", () => { avd: avdEmulatorName, geny: genyEmulatorName, }); - const emulatorName = await androidEmulatorServices.getRunningEmulatorName( - avdEmulatorName - ); + const emulatorName = + await androidEmulatorServices.getRunningEmulatorName(avdEmulatorName); assert.deepStrictEqual(emulatorName, avdEmulatorName); }); }); describe("startEmulator", () => { function mockStartEmulator( - deviceInfo: Mobile.IDeviceInfo + deviceInfo: Mobile.IDeviceInfo, ): Mobile.IDeviceInfo { if (deviceInfo.vendor === "Avd") { androidVirtualDeviceService.startEmulatorArgs = () => []; @@ -249,7 +247,7 @@ describe("androidEmulatorService", () => { mockGetRunningEmulatorIds([], []); mockGetEmulatorImages( { devices: [avdEmulator], errors: [] }, - { devices: [], errors: [] } + { devices: [], errors: [] }, ); const deviceInfo = mockStartEmulator(avdEmulator); await androidEmulatorServices.startEmulator(avdEmulator); @@ -259,7 +257,7 @@ describe("androidEmulatorService", () => { mockGetRunningEmulatorIds([], []); mockGetEmulatorImages( { devices: [], errors: [] }, - { devices: [genyEmulator], errors: [] } + { devices: [genyEmulator], errors: [] }, ); const deviceInfo = mockStartEmulator(genyEmulator); assert.deepStrictEqual(deviceInfo, genyEmulator); diff --git a/lib/common/test/unit-tests/mobile/android/logcat-helper.ts b/lib/common/test/unit-tests/mobile/android/logcat-helper.ts index 7bbaf6540a..e9107f97ef 100644 --- a/lib/common/test/unit-tests/mobile/android/logcat-helper.ts +++ b/lib/common/test/unit-tests/mobile/android/logcat-helper.ts @@ -1,3 +1,4 @@ +import { withDone } from "../../../with-done"; import { LogcatHelper } from "../../../../mobile/android/logcat-helper"; import { Yok } from "../../../../yok"; import { assert } from "chai"; @@ -39,7 +40,7 @@ class ChildProcessStub { public spawn( command: string, args?: string[], - options?: any + options?: any, ): childProcess.ChildProcess { this.adbProcessArgs = args; this.processSpawnCallCount++; @@ -99,7 +100,7 @@ function createTestInjector(): IInjector { function startLogcatHelper( injector: IInjector, - startOptions: { deviceIdentifier: string; pid?: string } + startOptions: { deviceIdentifier: string; pid?: string }, ) { const logcatHelper = injector.resolve("logcatHelper"); /* tslint:disable:no-floating-promises */ @@ -120,85 +121,94 @@ describe("logcat-helper", () => { }); describe("start", () => { - it("should read the whole logcat correctly", (done: mocha.Done) => { - injector.register("deviceLogProvider", { - logData( - line: string, - platform: string, - deviceIdentifier: string - ): void { - loggedData.push(line); - if (line === "end") { - assert.isAbove(loggedData.length, 0); - done(); - } - }, - }); - - startLogcatHelper(injector, { deviceIdentifier: validIdentifier }); - }); - - it("should pass the pid filter to the adb process", (done: mocha.Done) => { - const expectedPid = "MyCoolPid"; - injector.register("deviceLogProvider", { - logData( - line: string, - platform: string, - deviceIdentifier: string - ): void { - loggedData.push(line); - if (line === "end") { - assert.equal( - childProcessStub.processSpawnCallCount, - PROCESS_COUNT_PER_DEVICE - ); - const adbProcessArgs = childProcessStub.spawnedProcesses[0].args; - assert.include(adbProcessArgs, `--pid=${expectedPid}`); - done(); - } - }, - }); + it( + "should read the whole logcat correctly", + withDone((done) => { + injector.register("deviceLogProvider", { + logData( + line: string, + platform: string, + deviceIdentifier: string, + ): void { + loggedData.push(line); + if (line === "end") { + assert.isAbove(loggedData.length, 0); + done(); + } + }, + }); - startLogcatHelper(injector, { - deviceIdentifier: validIdentifier, - pid: expectedPid, - }); - }); + startLogcatHelper(injector, { deviceIdentifier: validIdentifier }); + }), + ); + + it( + "should pass the pid filter to the adb process", + withDone((done) => { + const expectedPid = "MyCoolPid"; + injector.register("deviceLogProvider", { + logData( + line: string, + platform: string, + deviceIdentifier: string, + ): void { + loggedData.push(line); + if (line === "end") { + assert.equal( + childProcessStub.processSpawnCallCount, + PROCESS_COUNT_PER_DEVICE, + ); + const adbProcessArgs = childProcessStub.spawnedProcesses[0].args; + assert.include(adbProcessArgs, `--pid=${expectedPid}`); + done(); + } + }, + }); - it("should not pass the pid filter to the adb process when Android version is less than 7", (done: mocha.Done) => { - const expectedPid = "MyCoolPid"; - injector.register("devicesService", { - getDevice: (): Mobile.IDevice => { - return { - deviceInfo: { - version: "6.0.0", - }, - }; - }, - }); + startLogcatHelper(injector, { + deviceIdentifier: validIdentifier, + pid: expectedPid, + }); + }), + ); + + it( + "should not pass the pid filter to the adb process when Android version is less than 7", + withDone((done) => { + const expectedPid = "MyCoolPid"; + injector.register("devicesService", { + getDevice: (): Mobile.IDevice => { + return { + deviceInfo: { + version: "6.0.0", + }, + }; + }, + }); - injector.register("deviceLogProvider", { - logData( - line: string, - platform: string, - deviceIdentifier: string - ): void { - loggedData.push(line); - if (line === "end") { - assert.notInclude( - childProcessStub.adbProcessArgs, - `--pid=${expectedPid}` - ); - done(); - } - }, - }); + injector.register("deviceLogProvider", { + logData( + line: string, + platform: string, + deviceIdentifier: string, + ): void { + loggedData.push(line); + if (line === "end") { + assert.notInclude( + childProcessStub.adbProcessArgs, + `--pid=${expectedPid}`, + ); + done(); + } + }, + }); - startLogcatHelper(injector, { - deviceIdentifier: validIdentifier, - pid: expectedPid, - }); - }); + startLogcatHelper(injector, { + deviceIdentifier: validIdentifier, + pid: expectedPid, + }); + }), + ); it("should start a single adb process when called multiple times with the same identifier", async () => { const logcatHelper = injector.resolve("logcatHelper"); @@ -215,7 +225,7 @@ describe("logcat-helper", () => { assert.equal( childProcessStub.processSpawnCallCount, - PROCESS_COUNT_PER_DEVICE + PROCESS_COUNT_PER_DEVICE, ); }); @@ -234,7 +244,7 @@ describe("logcat-helper", () => { assert.equal( childProcessStub.processSpawnCallCount, - 3 * PROCESS_COUNT_PER_DEVICE + 3 * PROCESS_COUNT_PER_DEVICE, ); }); }); @@ -247,7 +257,7 @@ describe("logcat-helper", () => { }); assert.equal( childProcessStub.processSpawnCallCount, - PROCESS_COUNT_PER_DEVICE + PROCESS_COUNT_PER_DEVICE, ); await logcatHelper.stop(validIdentifier); await logcatHelper.start({ @@ -256,7 +266,7 @@ describe("logcat-helper", () => { assert.equal( childProcessStub.processSpawnCallCount, - 2 * PROCESS_COUNT_PER_DEVICE + 2 * PROCESS_COUNT_PER_DEVICE, ); }); @@ -304,7 +314,7 @@ describe("logcat-helper", () => { assert.equal( childProcessStub.processSpawnCallCount, - PROCESS_COUNT_PER_DEVICE + PROCESS_COUNT_PER_DEVICE, ); childProcessStub.spawnedProcesses.forEach((spawnedProcess) => { @@ -317,7 +327,7 @@ describe("logcat-helper", () => { assert.equal( childProcessStub.processSpawnCallCount, - 2 * PROCESS_COUNT_PER_DEVICE + 2 * PROCESS_COUNT_PER_DEVICE, ); }); } diff --git a/lib/common/test/unit-tests/mobile/application-manager-base.ts b/lib/common/test/unit-tests/mobile/application-manager-base.ts index 262c421455..c81200dd97 100644 --- a/lib/common/test/unit-tests/mobile/application-manager-base.ts +++ b/lib/common/test/unit-tests/mobile/application-manager-base.ts @@ -1,3 +1,4 @@ +import { withDone } from "../../with-done"; import { Yok } from "../../../yok"; import { assert } from "chai"; import * as _ from "lodash"; @@ -314,292 +315,318 @@ describe("ApplicationManagerBase", () => { await Promise.all([foundAppsPromise, lostAppsPromise]); }); - it("emits debuggableViewFound when new views are available for debug", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); - const numberOfViewsPerApp = 2; - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - numberOfViewsPerApp, - ); - const currentDebuggableViews: IDictionary = - {}; - applicationManager.on( - "debuggableViewFound", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - currentDebuggableViews[appIdentifier] = - currentDebuggableViews[appIdentifier] || []; - currentDebuggableViews[appIdentifier].push(d); - const numberOfFoundViewsPerApp = _.uniq( - _.values(currentDebuggableViews).map((arr) => arr.length), - ); - if ( - _.keys(currentDebuggableViews).length === - currentlyAvailableAppsForDebugging.length && - numberOfFoundViewsPerApp.length === 1 && // for all apps we've found exactly two apps. - numberOfFoundViewsPerApp[0] === numberOfViewsPerApp - ) { - _.each(currentDebuggableViews, (webViews, appId) => { - _.each(webViews, (webView) => { - const expectedWebView = _.find( - currentlyAvailableAppWebViewsForDebugging[appId], - (c) => c.id === webView.id, - ); - assert.isTrue(_.isEqual(webView, expectedWebView)); + it( + "emits debuggableViewFound when new views are available for debug", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(2); + const numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + numberOfViewsPerApp, + ); + const currentDebuggableViews: IDictionary< + Mobile.IDebugWebViewInfo[] + > = {}; + applicationManager.on( + "debuggableViewFound", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + currentDebuggableViews[appIdentifier] = + currentDebuggableViews[appIdentifier] || []; + currentDebuggableViews[appIdentifier].push(d); + const numberOfFoundViewsPerApp = _.uniq( + _.values(currentDebuggableViews).map((arr) => arr.length), + ); + if ( + _.keys(currentDebuggableViews).length === + currentlyAvailableAppsForDebugging.length && + numberOfFoundViewsPerApp.length === 1 && // for all apps we've found exactly two apps. + numberOfFoundViewsPerApp[0] === numberOfViewsPerApp + ) { + _.each(currentDebuggableViews, (webViews, appId) => { + _.each(webViews, (webView) => { + const expectedWebView = _.find( + currentlyAvailableAppWebViewsForDebugging[appId], + (c) => c.id === webView.id, + ); + assert.isTrue(_.isEqual(webView, expectedWebView)); + }); }); - }); - setTimeout(done, 0); - } - }, - ); + setTimeout(done, 0); + } + }, + ); - /* tslint:disable:no-floating-promises */ - applicationManager.checkForApplicationUpdates(); - /* tslint:enable:no-floating-promises */ - }); + /* tslint:disable:no-floating-promises */ + applicationManager.checkForApplicationUpdates(); + /* tslint:enable:no-floating-promises */ + }), + ); - it("emits debuggableViewLost when views for debug are removed", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); - const numberOfViewsPerApp = 2; - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - numberOfViewsPerApp, - ); - const expectedResults = _.cloneDeep( - currentlyAvailableAppWebViewsForDebugging, - ); - const currentDebuggableViews: IDictionary = - {}; - - applicationManager - .checkForApplicationUpdates() - .then(() => { - applicationManager.on( - "debuggableViewLost", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - currentDebuggableViews[appIdentifier] = - currentDebuggableViews[appIdentifier] || []; - currentDebuggableViews[appIdentifier].push(d); - const numberOfFoundViewsPerApp = _.uniq( - _.values(currentDebuggableViews).map((arr) => arr.length), - ); - if ( - _.keys(currentDebuggableViews).length === - currentlyAvailableAppsForDebugging.length && - numberOfFoundViewsPerApp.length === 1 && // for all apps we've found exactly two apps. - numberOfFoundViewsPerApp[0] === numberOfViewsPerApp - ) { - _.each(currentDebuggableViews, (webViews, appId) => { - _.each(webViews, (webView) => { - const expectedWebView = _.find( - expectedResults[appId], - (c) => c.id === webView.id, - ); - assert.isTrue(_.isEqual(webView, expectedWebView)); + it( + "emits debuggableViewLost when views for debug are removed", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(2); + const numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + numberOfViewsPerApp, + ); + const expectedResults = _.cloneDeep( + currentlyAvailableAppWebViewsForDebugging, + ); + const currentDebuggableViews: IDictionary< + Mobile.IDebugWebViewInfo[] + > = {}; + + applicationManager + .checkForApplicationUpdates() + .then(() => { + applicationManager.on( + "debuggableViewLost", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + currentDebuggableViews[appIdentifier] = + currentDebuggableViews[appIdentifier] || []; + currentDebuggableViews[appIdentifier].push(d); + const numberOfFoundViewsPerApp = _.uniq( + _.values(currentDebuggableViews).map((arr) => arr.length), + ); + if ( + _.keys(currentDebuggableViews).length === + currentlyAvailableAppsForDebugging.length && + numberOfFoundViewsPerApp.length === 1 && // for all apps we've found exactly two apps. + numberOfFoundViewsPerApp[0] === numberOfViewsPerApp + ) { + _.each(currentDebuggableViews, (webViews, appId) => { + _.each(webViews, (webView) => { + const expectedWebView = _.find( + expectedResults[appId], + (c) => c.id === webView.id, + ); + assert.isTrue(_.isEqual(webView, expectedWebView)); + }); }); - }); - setTimeout(done, 0); - } - }, - ); - - currentlyAvailableAppWebViewsForDebugging = _.mapValues( - currentlyAvailableAppWebViewsForDebugging, - (a) => [] as any, - ); - return applicationManager.checkForApplicationUpdates(); - }) - .catch(); - }); + setTimeout(done, 0); + } + }, + ); - it("emits debuggableViewFound when new views are available for debug", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); - const numberOfViewsPerApp = 2; - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - numberOfViewsPerApp, - ); - let expectedViewToBeFound = createDebuggableWebView("uniqueId"); - let expectedAppIdentifier = - currentlyAvailableAppsForDebugging[0].appIdentifier; - let isLastCheck = false; - - applicationManager - .checkForApplicationUpdates() - .then(() => { - applicationManager.on( - "debuggableViewFound", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - assert.deepStrictEqual(appIdentifier, expectedAppIdentifier); - assert.isTrue(_.isEqual(d, expectedViewToBeFound)); - - if (isLastCheck) { - setTimeout(done, 0); - } - }, - ); + currentlyAvailableAppWebViewsForDebugging = _.mapValues( + currentlyAvailableAppWebViewsForDebugging, + (a) => [] as any, + ); + return applicationManager.checkForApplicationUpdates(); + }) + .catch(); + }), + ); - currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].push(_.cloneDeep(expectedViewToBeFound)); - return applicationManager.checkForApplicationUpdates(); - }) - .catch() - .then(() => { - expectedViewToBeFound = createDebuggableWebView("uniqueId1"); - currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].push(_.cloneDeep(expectedViewToBeFound)); - return applicationManager.checkForApplicationUpdates(); - }) - .catch() - .then(() => { - expectedViewToBeFound = createDebuggableWebView("uniqueId2"); - expectedAppIdentifier = - currentlyAvailableAppsForDebugging[1].appIdentifier; - isLastCheck = true; + it( + "emits debuggableViewFound when new views are available for debug", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(2); + const numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + numberOfViewsPerApp, + ); + let expectedViewToBeFound = createDebuggableWebView("uniqueId"); + let expectedAppIdentifier = + currentlyAvailableAppsForDebugging[0].appIdentifier; + let isLastCheck = false; + + applicationManager + .checkForApplicationUpdates() + .then(() => { + applicationManager.on( + "debuggableViewFound", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.deepStrictEqual(appIdentifier, expectedAppIdentifier); + assert.isTrue(_.isEqual(d, expectedViewToBeFound)); + + if (isLastCheck) { + setTimeout(done, 0); + } + }, + ); - currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].push(_.cloneDeep(expectedViewToBeFound)); - return applicationManager.checkForApplicationUpdates(); - }) - .catch(); - }); + currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].push(_.cloneDeep(expectedViewToBeFound)); + return applicationManager.checkForApplicationUpdates(); + }) + .catch() + .then(() => { + expectedViewToBeFound = createDebuggableWebView("uniqueId1"); + currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].push(_.cloneDeep(expectedViewToBeFound)); + return applicationManager.checkForApplicationUpdates(); + }) + .catch() + .then(() => { + expectedViewToBeFound = createDebuggableWebView("uniqueId2"); + expectedAppIdentifier = + currentlyAvailableAppsForDebugging[1].appIdentifier; + isLastCheck = true; + + currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].push(_.cloneDeep(expectedViewToBeFound)); + return applicationManager.checkForApplicationUpdates(); + }) + .catch(); + }), + ); - it("emits debuggableViewLost when views for debug are not available anymore", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(2); - const numberOfViewsPerApp = 2; - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - numberOfViewsPerApp, - ); - let expectedAppIdentifier = - currentlyAvailableAppsForDebugging[0].appIdentifier; - let expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].splice(0, 1)[0]; - let isLastCheck = false; - - applicationManager - .checkForApplicationUpdates() - .then(() => { - applicationManager.on( - "debuggableViewLost", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - assert.deepStrictEqual(appIdentifier, expectedAppIdentifier); - assert.isTrue(_.isEqual(d, expectedViewToBeLost)); - - if (isLastCheck) { - setTimeout(done, 0); - } - }, - ); + it( + "emits debuggableViewLost when views for debug are not available anymore", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(2); + const numberOfViewsPerApp = 2; + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + numberOfViewsPerApp, + ); + let expectedAppIdentifier = + currentlyAvailableAppsForDebugging[0].appIdentifier; + let expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].splice(0, 1)[0]; + let isLastCheck = false; + + applicationManager + .checkForApplicationUpdates() + .then(() => { + applicationManager.on( + "debuggableViewLost", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.deepStrictEqual(appIdentifier, expectedAppIdentifier); + assert.isTrue(_.isEqual(d, expectedViewToBeLost)); + + if (isLastCheck) { + setTimeout(done, 0); + } + }, + ); - return applicationManager.checkForApplicationUpdates(); - }) - .catch() - .then(() => { - expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].splice(0, 1)[0]; - return applicationManager.checkForApplicationUpdates(); - }) - .catch() - .then(() => { - expectedAppIdentifier = - currentlyAvailableAppsForDebugging[1].appIdentifier; - expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ - expectedAppIdentifier - ].splice(0, 1)[0]; - - isLastCheck = true; - return applicationManager.checkForApplicationUpdates(); - }) - .catch(); - }); + return applicationManager.checkForApplicationUpdates(); + }) + .catch() + .then(() => { + expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].splice(0, 1)[0]; + return applicationManager.checkForApplicationUpdates(); + }) + .catch() + .then(() => { + expectedAppIdentifier = + currentlyAvailableAppsForDebugging[1].appIdentifier; + expectedViewToBeLost = currentlyAvailableAppWebViewsForDebugging[ + expectedAppIdentifier + ].splice(0, 1)[0]; + + isLastCheck = true; + return applicationManager.checkForApplicationUpdates(); + }) + .catch(); + }), + ); - it("emits debuggableViewChanged when view's property is modified (each one except id)", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(1); - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - 2, - ); - const viewToChange = - currentlyAvailableAppWebViewsForDebugging[ - currentlyAvailableAppsForDebugging[0].appIdentifier - ][0]; - const expectedView = _.cloneDeep(viewToChange); - expectedView.title = "new title"; + it( + "emits debuggableViewChanged when view's property is modified (each one except id)", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(1); + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + 2, + ); + const viewToChange = + currentlyAvailableAppWebViewsForDebugging[ + currentlyAvailableAppsForDebugging[0].appIdentifier + ][0]; + const expectedView = _.cloneDeep(viewToChange); + expectedView.title = "new title"; - applicationManager.on( - "debuggableViewChanged", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - assert.isTrue(_.isEqual(d, expectedView)); - setTimeout(done, 0); - }, - ); + applicationManager.on( + "debuggableViewChanged", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.isTrue(_.isEqual(d, expectedView)); + setTimeout(done, 0); + }, + ); - applicationManager - .checkForApplicationUpdates() - .then(() => { - viewToChange.title = "new title"; - return applicationManager.checkForApplicationUpdates(); - }) - .catch(); - }); + applicationManager + .checkForApplicationUpdates() + .then(() => { + viewToChange.title = "new title"; + return applicationManager.checkForApplicationUpdates(); + }) + .catch(); + }), + ); - it("does not emit debuggableViewChanged when id is modified", (done: mocha.Done) => { - currentlyAvailableAppsForDebugging = createAppsAvailableForDebugging(1); - currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( - currentlyAvailableAppsForDebugging, - 2, - ); - const viewToChange = - currentlyAvailableAppWebViewsForDebugging[ - currentlyAvailableAppsForDebugging[0].appIdentifier - ][0]; - const expectedView = _.cloneDeep(viewToChange); - - applicationManager - .checkForApplicationUpdates() - .then(() => { - applicationManager.on( - "debuggableViewChanged", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - setTimeout( - () => - done( - new Error( - "When id is changed, debuggableViewChanged must not be emitted.", + it( + "does not emit debuggableViewChanged when id is modified", + withDone((done) => { + currentlyAvailableAppsForDebugging = + createAppsAvailableForDebugging(1); + currentlyAvailableAppWebViewsForDebugging = createDebuggableWebViews( + currentlyAvailableAppsForDebugging, + 2, + ); + const viewToChange = + currentlyAvailableAppWebViewsForDebugging[ + currentlyAvailableAppsForDebugging[0].appIdentifier + ][0]; + const expectedView = _.cloneDeep(viewToChange); + + applicationManager + .checkForApplicationUpdates() + .then(() => { + applicationManager.on( + "debuggableViewChanged", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + setTimeout( + () => + done( + new Error( + "When id is changed, debuggableViewChanged must not be emitted.", + ), ), - ), - 0, - ); - }, - ); + 0, + ); + }, + ); - applicationManager.on( - "debuggableViewLost", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - assert.isTrue(_.isEqual(d, expectedView)); - }, - ); + applicationManager.on( + "debuggableViewLost", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + assert.isTrue(_.isEqual(d, expectedView)); + }, + ); - applicationManager.on( - "debuggableViewFound", - (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { - expectedView.id = "new id"; - assert.isTrue(_.isEqual(d, expectedView)); - setTimeout(done, 0); - }, - ); + applicationManager.on( + "debuggableViewFound", + (appIdentifier: string, d: Mobile.IDebugWebViewInfo) => { + expectedView.id = "new id"; + assert.isTrue(_.isEqual(d, expectedView)); + setTimeout(done, 0); + }, + ); - viewToChange.id = "new id"; - }) - .catch() - .then(() => applicationManager.checkForApplicationUpdates()) - .catch(); - }); + viewToChange.id = "new id"; + }) + .catch() + .then(() => applicationManager.checkForApplicationUpdates()) + .catch(); + }), + ); }); describe("installed and uninstalled apps", () => { diff --git a/lib/common/test/unit-tests/mobile/device-log-provider.ts b/lib/common/test/unit-tests/mobile/device-log-provider.ts index 022524d329..46f6929a90 100644 --- a/lib/common/test/unit-tests/mobile/device-log-provider.ts +++ b/lib/common/test/unit-tests/mobile/device-log-provider.ts @@ -71,7 +71,7 @@ const createTestInjector = (): IInjector => { "..", "resources", "device-log-provider-integration-tests", - pl.toLowerCase() + pl.toLowerCase(), ), frameworkPackageName: `tns-${platform.toLowerCase()}`, }; @@ -112,7 +112,7 @@ describe("deviceLogProvider", () => { assert.equal(actualFixed, expectedFixed); }; - before(async () => { + beforeAll(async () => { testInjector = createTestInjector(); const fs = testInjector.resolve("fs"); const logSourceMapService = testInjector.resolve("logSourceMapService"); @@ -121,7 +121,7 @@ describe("deviceLogProvider", () => { "..", "..", "resources", - "device-log-provider-integration-tests" + "device-log-provider-integration-tests", ); const files = fs.enumerateFilesInDirectorySync(originalFilesLocation); for (const file of files) { @@ -133,7 +133,7 @@ describe("deviceLogProvider", () => { testInjector.resolve("deviceLogProvider"); deviceLogProvider.setProjectDirForDevice( "deviceIdentifier", - "dir_with_runtime_6.1.0" + "dir_with_runtime_6.1.0", ); }); @@ -151,28 +151,28 @@ describe("deviceLogProvider", () => { } }; - before(() => { + beforeAll(() => { platform = "android"; deviceLogProvider.setApplicationPidForDevice(deviceIdentifier, "25038"); }); describe("runtime version is below 6.1.0", () => { - before(() => { + beforeAll(() => { runtimeVersion = "6.0.0"; deviceLogProvider.setProjectDirForDevice( "deviceIdentifier", - "dir_with_runtime_6.0.0" + "dir_with_runtime_6.0.0", ); }); describe("SDK 28", () => { it("console.log", () => { logDataForAndroid( - "08-22 15:31:53.189 25038 25038 I JS : HMR: Hot Module Replacement Enabled. Waiting for signal." + "08-22 15:31:53.189 25038 25038 I JS : HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -204,7 +204,7 @@ level0_0: { level0_1: { "level1_0": "value3" } -==== object dump end ====\n` +==== object dump end ====\n`, ); }); @@ -218,7 +218,7 @@ level0_1: { `multiline message from - console.log\n` + console.log\n`, ); }); @@ -234,13 +234,13 @@ level0_1: { at viewModel.onTap file: app/main-view-model.js:39:0 at push.../node_modules/tns-core-modules/data/observable/observable.js.Observable.notify file: node_modules/tns-core-modules/data/observable/observable.js:107:0 at push.../node_modules/tns-core-modules/data/observable/observable.js.Observable._emit file: node_modules/tns-core-modules/data/observable/observable.js:127:0 -at ClickListenerImpl.onClick file: node_modules/tns-core-modules/ui/button/button.js:29:0\n` +at ClickListenerImpl.onClick file: node_modules/tns-core-modules/ui/button/button.js:29:0\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForAndroid( - "08-22 15:32:03.145 25038 25038 I JS : console.time: 9603.00ms" + "08-22 15:32:03.145 25038 25038 I JS : console.time: 9603.00ms", ); assertData(logger.output, "console.time: 9603.00ms\n"); }); @@ -298,7 +298,7 @@ at ClickListenerImpl.onClick file: node_modules/tns-core-modules/ui/button/butto 08-22 15:32:03.211 25038 25038 W System.err: at android.app.ActivityThread.main(ActivityThread.java:6669) 08-22 15:32:03.211 25038 25038 W System.err: at java.lang.reflect.Method.invoke(Native Method) 08-22 15:32:03.211 25038 25038 W System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) -08-22 15:32:03.211 25038 25038 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)` +08-22 15:32:03.211 25038 25038 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)`, ); assertData( @@ -330,29 +330,29 @@ System.err: at android.os.Looper.loop(Looper.java:193) System.err: at android.app.ActivityThread.main(ActivityThread.java:6669) System.err: at java.lang.reflect.Method.invoke(Native Method) System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) -System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n` +System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n`, ); }); }); }); describe("runtime version is 6.1.0 or later", () => { - before(() => { + beforeAll(() => { runtimeVersion = "6.1.0"; deviceLogProvider.setProjectDirForDevice( "deviceIdentifier", - "dir_with_runtime_6.1.0" + "dir_with_runtime_6.1.0", ); }); describe("SDK 28", () => { it("console.log", () => { logDataForAndroid( - "08-23 16:15:55.254 25038 25038 I JS : HMR: Hot Module Replacement Enabled. Waiting for signal." + "08-23 16:15:55.254 25038 25038 I JS : HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -384,7 +384,7 @@ level0_0: { level0_1: { "level1_0": "value3" } -==== object dump end ====\n` +==== object dump end ====\n`, ); }); @@ -398,7 +398,7 @@ level0_1: { `multiline message from - console.log\n` + console.log\n`, ); }); @@ -414,13 +414,13 @@ level0_1: { at viewModel.onTap (file: app/main-view-model.js:39:0) at push.../node_modules/tns-core-modules/data/observable/observable.js.Observable.notify (file: node_modules/tns-core-modules/data/observable/observable.js:107:0) at push.../node_modules/tns-core-modules/data/observable/observable.js.Observable._emit (file: node_modules/tns-core-modules/data/observable/observable.js:127:0) -at ClickListenerImpl.onClick (file: node_modules/tns-core-modules/ui/button/button.js:29:0)\n` +at ClickListenerImpl.onClick (file: node_modules/tns-core-modules/ui/button/button.js:29:0)\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForAndroid( - "08-23 16:16:06.571 25038 25038 I JS : console.time: 9510.00ms" + "08-23 16:16:06.571 25038 25038 I JS : console.time: 9510.00ms", ); assertData(logger.output, "console.time: 9510.00ms\n"); }); @@ -478,7 +478,7 @@ at ClickListenerImpl.onClick (file: node_modules/tns-core-modules/ui/button/butt 08-23 16:16:06.799 25038 25038 W System.err: at android.app.ActivityThread.main(ActivityThread.java:6669) 08-23 16:16:06.799 25038 25038 W System.err: at java.lang.reflect.Method.invoke(Native Method) 08-23 16:16:06.799 25038 25038 W System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) -08-23 16:16:06.799 25038 25038 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)` +08-23 16:16:06.799 25038 25038 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)`, ); assertData( @@ -510,7 +510,7 @@ System.err: at android.os.Looper.loop(Looper.java:193) System.err: at android.app.ActivityThread.main(ActivityThread.java:6669) System.err: at java.lang.reflect.Method.invoke(Native Method) System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) -System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n` +System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n`, ); }); }); @@ -518,11 +518,11 @@ System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n` }); describe("iOS", () => { - before(() => { + beforeAll(() => { platform = "ios"; deviceLogProvider.setProjectNameForDevice( deviceIdentifier, - "appTestLogs" + "appTestLogs", ); }); @@ -531,11 +531,11 @@ System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n` }; describe("runtime version is below 6.1.0", () => { - before(() => { + beforeAll(() => { runtimeVersion = "6.0.0"; deviceLogProvider.setProjectDirForDevice( "deviceIdentifier", - "dir_with_runtime_6.0.0" + "dir_with_runtime_6.0.0", ); }); @@ -543,12 +543,12 @@ System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)\n` describe("simulator output", () => { it("console.log", () => { logDataForiOS( - "Aug 23 14:38:54 mcsofvladimirov appTestLogs[8455]: CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal." + "Aug 23 14:38:54 mcsofvladimirov appTestLogs[8455]: CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0 HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0 HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -567,11 +567,11 @@ level0_1: { } ==== object dump end ====`; logDataForiOS( - `Aug 23 14:38:58 mcsofvladimirov appTestLogs[8455]: CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}` + `Aug 23 14:38:58 mcsofvladimirov appTestLogs[8455]: CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}`, ); assertData( logger.output, - `CONSOLE LOG file: app/main-view-model.js:20:0\n${dump}\n` + `CONSOLE LOG file: app/main-view-model.js:20:0\n${dump}\n`, ); }); @@ -582,7 +582,7 @@ level0_1: { `\tmessage`, `\sfrom`, `\t\sconsole.log`, - ].join("\n") + ].join("\n"), ); assertData( logger.output, @@ -591,7 +591,7 @@ level0_1: { `\tmessage`, `\sfrom`, `\t\sconsole.log\n`, - ].join("\n") + ].join("\n"), ); }); @@ -631,17 +631,17 @@ level0_1: { 13 anonymous@file:///app/bundle.js:2:61 14 evaluate@[native code] 15 moduleEvaluation@:1:11 -16 promiseReactionJob@:1:11\n` +16 promiseReactionJob@:1:11\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForiOS( - `file:///app/main-view-model.js:41:0 CONSOLE INFO console.time: 3152.344ms` + `file:///app/main-view-model.js:41:0 CONSOLE INFO console.time: 3152.344ms`, ); assertData( logger.output, - "file:///app/main-view-model.js:41:0 CONSOLE INFO console.time: 3152.344ms\n" + "file:///app/main-view-model.js:41:0 CONSOLE INFO console.time: 3152.344ms\n", ); }); @@ -810,7 +810,7 @@ JS Stack: 9 anonymous@file:///app/bundle.js:2:61 10 evaluate@[native code] 11 moduleEvaluation@:1:11 - 12 promiseReactionJob@:1:11\n` + 12 promiseReactionJob@:1:11\n`, ); }); }); @@ -820,12 +820,12 @@ JS Stack: describe("simulator output", () => { it("console.log", () => { logDataForiOS( - "2019-08-22 18:21:24.066975+0300 localhost appTestLogs[55619]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal." + "2019-08-22 18:21:24.066975+0300 localhost appTestLogs[55619]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0 HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0 HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -844,11 +844,11 @@ level0_1: { } ==== object dump end ====`; logDataForiOS( - `2019-08-22 18:21:26.133151+0300 localhost appTestLogs[55619]: (NativeScript) CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}` + `2019-08-22 18:21:26.133151+0300 localhost appTestLogs[55619]: (NativeScript) CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}`, ); assertData( logger.output, - `CONSOLE LOG file: app/main-view-model.js:20:0\n${dump}\n` + `CONSOLE LOG file: app/main-view-model.js:20:0\n${dump}\n`, ); }); @@ -859,7 +859,7 @@ level0_1: { `message`, ` from`, `console.log`, - ].join("\n") + ].join("\n"), ); assertData( logger.output, @@ -868,7 +868,7 @@ level0_1: { `message`, ` from`, `console.log\n`, - ].join("\n") + ].join("\n"), ); }); @@ -908,17 +908,17 @@ level0_1: { 13 anonymous@file:///app/bundle.js:2:61 14 evaluate@[native code] 15 moduleEvaluation@:1:11 -16 promiseReactionJob@:1:11\n` +16 promiseReactionJob@:1:11\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForiOS( - `2019-08-22 18:21:26.133972+0300 localhost appTestLogs[55619]: (NativeScript) file:///app/bundle.js:291:24: CONSOLE INFO console.time: 1988.737ms` + `2019-08-22 18:21:26.133972+0300 localhost appTestLogs[55619]: (NativeScript) file:///app/bundle.js:291:24: CONSOLE INFO console.time: 1988.737ms`, ); assertData( logger.output, - "file: app/main-view-model.js:41:0 CONSOLE INFO console.time: 1988.737ms\n" + "file: app/main-view-model.js:41:0 CONSOLE INFO console.time: 1988.737ms\n", ); }); @@ -1093,7 +1093,7 @@ JS Stack: 9 anonymous@file:///app/bundle.js:2:61 10 evaluate@[native code] 11 moduleEvaluation@:1:11 -12 promiseReactionJob@:1:11\n` +12 promiseReactionJob@:1:11\n`, ); }); }); @@ -1101,12 +1101,12 @@ JS Stack: }); describe("runtime version is 6.1.0 or later", () => { - before(() => { + beforeAll(() => { runtimeVersion = "6.1.0"; // set this, so the caching in logSourceMapService will detect correct runtime deviceLogProvider.setProjectDirForDevice( "deviceIdentifier", - "dir_with_runtime_6.1.0" + "dir_with_runtime_6.1.0", ); }); @@ -1114,12 +1114,12 @@ JS Stack: describe("simulator output", () => { it("console.log", () => { logDataForiOS( - "Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal." + "Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0: HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0: HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -1138,11 +1138,11 @@ level0_1: { } ==== object dump end ====`; logDataForiOS( - `Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}` + `Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}`, ); assertData( logger.output, - `CONSOLE LOG file: app/main-view-model.js:20:0:\n${dump}\n` + `CONSOLE LOG file: app/main-view-model.js:20:0:\n${dump}\n`, ); }); @@ -1153,7 +1153,7 @@ level0_1: { `message`, ` from`, `console.log`, - ].join("\n") + ].join("\n"), ); assertData( logger.output, @@ -1162,7 +1162,7 @@ level0_1: { `message`, ` from`, `console.log\n`, - ].join("\n") + ].join("\n"), ); }); @@ -1202,17 +1202,17 @@ at webpackJsonpCallback(file: app/webpack/bootstrap:30:0) at anonymous(file:///app/bundle.js:2:61) at evaluate([native code]) at moduleEvaluation -at promiseReactionJob\n` +at promiseReactionJob\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForiOS( - `Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: file:///app/bundle.js:291:24: CONSOLE INFO console.time: 27523.877ms` + `Aug 23 18:12:39 mcsofvladimirov appTestLogs[29554]: file:///app/bundle.js:291:24: CONSOLE INFO console.time: 27523.877ms`, ); assertData( logger.output, - "file: app/main-view-model.js:41:0: CONSOLE INFO console.time: 27523.877ms\n" + "file: app/main-view-model.js:41:0: CONSOLE INFO console.time: 27523.877ms\n", ); }); @@ -1381,7 +1381,7 @@ UIApplicationMain([native code]) at anonymous(file:///app/bundle.js:2:61) at evaluate([native code]) at moduleEvaluation - at promiseReactionJob\n` + at promiseReactionJob\n`, ); }); }); @@ -1391,12 +1391,12 @@ UIApplicationMain([native code]) describe("simulator output", () => { it("console.log", () => { logDataForiOS( - "2019-08-23 17:08:38.860441+0300 localhost appTestLogs[21053]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal." + "2019-08-23 17:08:38.860441+0300 localhost appTestLogs[21053]: (NativeScript) CONSOLE INFO file:///app/vendor.js:168:36: HMR: Hot Module Replacement Enabled. Waiting for signal.", ); assertData( logger.output, - "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0: HMR: Hot Module Replacement Enabled. Waiting for signal.\n" + "CONSOLE INFO file: node_modules/nativescript-dev-webpack/hot.js:3:0: HMR: Hot Module Replacement Enabled. Waiting for signal.\n", ); }); @@ -1415,11 +1415,11 @@ level0_1: { } ==== object dump end ====`; logDataForiOS( - `2019-08-23 17:08:45.217971+0300 localhost appTestLogs[21053]: (NativeScript) CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}` + `2019-08-23 17:08:45.217971+0300 localhost appTestLogs[21053]: (NativeScript) CONSOLE LOG file:///app/bundle.js:270:20:\n${dump}`, ); assertData( logger.output, - `CONSOLE LOG file: app/main-view-model.js:20:0:\n${dump}\n` + `CONSOLE LOG file: app/main-view-model.js:20:0:\n${dump}\n`, ); }); @@ -1430,7 +1430,7 @@ level0_1: { `message`, ` from`, `console.log`, - ].join("\n") + ].join("\n"), ); assertData( logger.output, @@ -1439,7 +1439,7 @@ level0_1: { `message`, ` from`, `console.log\n`, - ].join("\n") + ].join("\n"), ); }); @@ -1479,17 +1479,17 @@ at webpackJsonpCallback(file: app/webpack/bootstrap:30:0) at anonymous(file:///app/bundle.js:2:61) at evaluate([native code]) at moduleEvaluation -at promiseReactionJob\n` +at promiseReactionJob\n`, ); }); it("console.time(timeEnd) statement", () => { logDataForiOS( - `2019-08-23 17:08:45.219341+0300 localhost appTestLogs[21053]: (NativeScript) file:///app/bundle.js:291:24: CONSOLE INFO console.time: 6285.199ms` + `2019-08-23 17:08:45.219341+0300 localhost appTestLogs[21053]: (NativeScript) file:///app/bundle.js:291:24: CONSOLE INFO console.time: 6285.199ms`, ); assertData( logger.output, - "file: app/main-view-model.js:41:0: CONSOLE INFO console.time: 6285.199ms\n" + "file: app/main-view-model.js:41:0: CONSOLE INFO console.time: 6285.199ms\n", ); }); @@ -1663,7 +1663,7 @@ at webpackJsonpCallback(file: app/webpack/bootstrap:30:0) at anonymous(file:///app/bundle.js:2:61) at evaluate([native code]) at moduleEvaluation -at promiseReactionJob\n` +at promiseReactionJob\n`, ); }); }); diff --git a/lib/common/test/unit-tests/mobile/devices-service.ts b/lib/common/test/unit-tests/mobile/devices-service.ts index d620079456..ae319ce0b9 100644 --- a/lib/common/test/unit-tests/mobile/devices-service.ts +++ b/lib/common/test/unit-tests/mobile/devices-service.ts @@ -1,3 +1,4 @@ +import { withDone } from "../../with-done"; import { DevicesService } from "../../../mobile/mobile-core/devices-service"; import { Yok } from "../../../yok"; import { @@ -460,73 +461,85 @@ describe("devicesService", () => { platform: "android", }; - it(`emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND} event when new Android Emulator image is found`, (done: mocha.Done) => { - const androidEmulatorDiscovery = - testInjector.resolve( - "androidEmulatorDiscovery", + it( + `emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND} event when new Android Emulator image is found`, + withDone((done) => { + const androidEmulatorDiscovery = + testInjector.resolve( + "androidEmulatorDiscovery", + ); + devicesService.on( + EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, + (emulatorImage: Mobile.IDeviceInfo) => { + assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); + done(); + }, ); - devicesService.on( - EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, - (emulatorImage: Mobile.IDeviceInfo) => { - assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); - done(); - }, - ); - androidEmulatorDiscovery.emit( - EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, - emulatorDataToEmit, - ); - }); + androidEmulatorDiscovery.emit( + EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, + emulatorDataToEmit, + ); + }), + ); - it(`emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND} when new iOS Simulator image is found`, (done: mocha.Done) => { - devicesService.on( - EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, - (emulatorImage: Mobile.IDeviceInfo) => { - assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); - done(); - }, - ); + it( + `emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND} when new iOS Simulator image is found`, + withDone((done) => { + devicesService.on( + EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, + (emulatorImage: Mobile.IDeviceInfo) => { + assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); + done(); + }, + ); - iOSSimulatorDiscovery.emit( - EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, - emulatorDataToEmit, - ); - }); + iOSSimulatorDiscovery.emit( + EmulatorDiscoveryNames.EMULATOR_IMAGE_FOUND, + emulatorDataToEmit, + ); + }), + ); - it(`emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST} event when new Android Emulator image is deleted`, (done: mocha.Done) => { - const androidEmulatorDiscovery = - testInjector.resolve( - "androidEmulatorDiscovery", + it( + `emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST} event when new Android Emulator image is deleted`, + withDone((done) => { + const androidEmulatorDiscovery = + testInjector.resolve( + "androidEmulatorDiscovery", + ); + devicesService.on( + EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, + (emulatorImage: Mobile.IDeviceInfo) => { + assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); + done(); + }, ); - devicesService.on( - EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, - (emulatorImage: Mobile.IDeviceInfo) => { - assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); - done(); - }, - ); - androidEmulatorDiscovery.emit( - EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, - emulatorDataToEmit, - ); - }); + androidEmulatorDiscovery.emit( + EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, + emulatorDataToEmit, + ); + }), + ); - it(`emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST} when iOS Simulator image is deleted`, (done: mocha.Done) => { - devicesService.on( - EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, - (emulatorImage: Mobile.IDeviceInfo) => { - assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); - done(); - }, - ); + it( + `emits ${EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST} when iOS Simulator image is deleted`, + withDone((done) => { + devicesService.on( + EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, + (emulatorImage: Mobile.IDeviceInfo) => { + assert.deepStrictEqual(emulatorImage, emulatorDataToEmit); + done(); + }, + ); - iOSSimulatorDiscovery.emit( - EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, - emulatorDataToEmit, - ); - }); + iOSSimulatorDiscovery.emit( + EmulatorDiscoveryNames.EMULATOR_IMAGE_LOST, + emulatorDataToEmit, + ); + }), + ); }); describe("startEmulatorIfNecessary behaves as expected:", () => { @@ -3102,7 +3115,7 @@ describe("devicesService", () => { helpers.isInteractive = originalIsInteractive; }); - after(() => { + afterAll(() => { helpers.isInteractive = originalIsInteractive; }); diff --git a/lib/common/test/unit-tests/services/hook-service.ts b/lib/common/test/unit-tests/services/hook-service.ts index f0eb83fd3f..f468018856 100644 --- a/lib/common/test/unit-tests/services/hook-service.ts +++ b/lib/common/test/unit-tests/services/hook-service.ts @@ -143,6 +143,157 @@ describe("hooks-service", () => { ); }); + it("should run hooks using syntax newer than ES2017", async () => { + const projectName = "projectDirectory"; + const projectPath = mkdtempSync(path.join(tmpdir(), `${projectName}-`)); + + const testInjector = createTestInjector({ projectDir: projectPath }); + + const script = [ + `class Message {`, + ` text = "after-prepare hook is running";`, + `}`, + `module.exports = function ($logger, hookArgs) {`, + ` const message = new Message();`, + ` $logger.info(message?.text ?? "fallback");`, + `};`, + ].join("\n"); + + fs.mkdirSync(path.join(projectPath, "hooks")); + fs.mkdirSync(path.join(projectPath, "hooks/after-prepare")); + fs.writeFileSync( + path.join(projectPath, "hooks/after-prepare/hook.js"), + script, + ); + + service = testInjector.resolve("$hooksService"); + + await service.executeAfterHooks("prepare", { hookArgs: {} }); + + assert.equal( + testInjector.resolve("$logger").output, + "after-prepare hook is running\n", + ); + }); + + it("should run hooks transpiled from a default export", async () => { + const projectName = "projectDirectory"; + const projectPath = mkdtempSync(path.join(tmpdir(), `${projectName}-`)); + + const testInjector = createTestInjector({ projectDir: projectPath }); + + const script = [ + `"use strict";`, + `Object.defineProperty(exports, "__esModule", { value: true });`, + `exports.default = function ($logger, hookArgs) {`, + ` $logger.info("after-prepare hook is running");`, + `};`, + ].join("\n"); + + fs.mkdirSync(path.join(projectPath, "hooks")); + fs.mkdirSync(path.join(projectPath, "hooks/after-prepare")); + fs.writeFileSync( + path.join(projectPath, "hooks/after-prepare/hook.js"), + script, + ); + + service = testInjector.resolve("$hooksService"); + + await service.executeAfterHooks("prepare", { hookArgs: {} }); + + assert.equal( + testInjector.resolve("$logger").output, + "after-prepare hook is running\n", + ); + }); + + it("should not run in-process hooks that do not export a function", async () => { + const projectName = "projectDirectory"; + const projectPath = mkdtempSync(path.join(tmpdir(), `${projectName}-`)); + + const testInjector = createTestInjector({ projectDir: projectPath }); + + const script = [`module.exports = { name: "not-a-hook" };`].join("\n"); + + fs.mkdirSync(path.join(projectPath, "hooks")); + fs.mkdirSync(path.join(projectPath, "hooks/after-prepare")); + fs.writeFileSync( + path.join(projectPath, "hooks/after-prepare/hook.js"), + script, + ); + + service = testInjector.resolve("$hooksService"); + + await service.executeAfterHooks("prepare", { hookArgs: {} }); + + expect(testInjector.resolve("$logger").warnOutput).to.have.string( + "does not export a function", + ); + }); + + describe("in-process detection", () => { + const shouldExecuteInProcess = (source: string): boolean => { + const testInjector = createTestInjector(); + const hooksService = testInjector.resolve("$hooksService"); + return (hooksService).shouldExecuteInProcess(source); + }; + + it("detects module.exports assignments alongside modern syntax", () => { + assert.isTrue( + shouldExecuteInProcess( + [ + `class Message { text = "hi"; }`, + `module.exports = function ($logger) {`, + ` $logger.info(new Message()?.text ?? "fallback");`, + `};`, + ].join("\n"), + ), + ); + }); + + it("detects exports.default assignments", () => { + assert.isTrue( + shouldExecuteInProcess( + [ + `"use strict";`, + `Object.defineProperty(exports, "__esModule", { value: true });`, + `exports.default = function ($logger) {};`, + ].join("\n"), + ), + ); + }); + + it("ignores scripts without an export assignment", () => { + assert.isFalse( + shouldExecuteInProcess( + [ + `var fs = require("fs");`, + `fs.writeFileSync("test.txt", "test");`, + ].join("\n"), + ), + ); + }); + + it("ignores nested and unrelated assignments", () => { + assert.isFalse( + shouldExecuteInProcess( + [ + `function register() {`, + ` module.exports = function () {};`, + `}`, + `exports.named = function () {};`, + `module.other = function () {};`, + ].join("\n"), + ), + ); + }); + + it("does not throw on unparseable sources", () => { + assert.isFalse(shouldExecuteInProcess(`}{ this is not ((javascript`)); + assert.isFalse(shouldExecuteInProcess(null)); + }); + }); + it("should run non-hook files", async () => { const projectName = "projectDirectory"; const projectPath = mkdtempSync(path.join(tmpdir(), `${projectName}-`)); diff --git a/lib/common/test/unit-tests/services/settings-service.ts b/lib/common/test/unit-tests/services/settings-service.ts index e072aea99f..a94a2cf2f0 100644 --- a/lib/common/test/unit-tests/services/settings-service.ts +++ b/lib/common/test/unit-tests/services/settings-service.ts @@ -27,14 +27,14 @@ describe("settingsService", () => { const appDataEnv = "appData"; const profileDirName = "profileDir"; - before(() => { + beforeAll(() => { // @ts-expect-error os.homedir = () => osHomedir; path.resolve = (p: string) => p; process.env.AppData = appDataEnv; }); - after(() => { + afterAll(() => { // @ts-expect-error os.homedir = originalOsHomedir; path.resolve = originalPathResolve; @@ -59,7 +59,7 @@ describe("settingsService", () => { }; const getExpectedProfileDir = ( - opts: { isWindows: boolean } = { isWindows: true } + opts: { isWindows: boolean } = { isWindows: true }, ) => { const defaultProfileDirLocation = opts.isWindows ? appDataEnv @@ -74,9 +74,8 @@ describe("settingsService", () => { const hostInfo = testInjector.resolve("hostInfo"); hostInfo.isWindows = isWindows; - const settingsService = testInjector.resolve( - SettingsService - ); + const settingsService = + testInjector.resolve(SettingsService); const actualProfileDir = settingsService.getProfileDir(); const expectedProfileDir = getExpectedProfileDir({ isWindows }); assert.equal(actualProfileDir, expectedProfileDir); @@ -122,21 +121,19 @@ describe("settingsService", () => { _.each(testData, (testCase) => { it(testCase.testName, () => { const testInjector = createTestInjector(); - const staticConfig = testInjector.resolve( - "staticConfig" - ); + const staticConfig = + testInjector.resolve("staticConfig"); staticConfig.USER_AGENT_NAME = defaultUserAgentName; - const settingsService = testInjector.resolve( - SettingsService - ); + const settingsService = + testInjector.resolve(SettingsService); settingsService.setSettings(testCase.dataPassedToSetSettings); const actualProfileDir = settingsService.getProfileDir(); assert.equal(actualProfileDir, testCase.expectedProfileDir); assert.equal( staticConfig.USER_AGENT_NAME, - testCase.expectedUserAgentName + testCase.expectedUserAgentName, ); }); }); diff --git a/lib/common/test/unit-tests/stubs.ts b/lib/common/test/unit-tests/stubs.ts index 3180fedc34..8bccdc038b 100644 --- a/lib/common/test/unit-tests/stubs.ts +++ b/lib/common/test/unit-tests/stubs.ts @@ -22,7 +22,7 @@ import { export class LockServiceStub implements ILockService { public async lock( lockFilePath?: string, - lockOpts?: ILockOptions + lockOpts?: ILockOptions, ): Promise<() => void> { return () => {}; } @@ -32,7 +32,7 @@ export class LockServiceStub implements ILockService { public async executeActionWithLock( action: () => Promise, lockFilePath?: string, - lockOpts?: ILockOptions + lockOpts?: ILockOptions, ): Promise { const result = await action(); return result; @@ -107,7 +107,7 @@ export class ErrorsStub implements IErrors { async beginCommand( action: () => Promise, - printHelpCommand: () => Promise + printHelpCommand: () => Promise, ): Promise { return action(); } @@ -120,8 +120,8 @@ export class ErrorsStub implements IErrors { } export class HooksServiceStub implements IHooksService { - async executeBeforeHooks(commandName: string): Promise { - return; + async executeBeforeHooks(commandName: string): Promise { + return []; } async executeAfterHooks(commandName: string): Promise { return; @@ -153,25 +153,25 @@ export class AndroidProcessServiceStub async mapAbstractToTcpPort( deviceIdentifier: string, appIdentifier: string, - framework: string + framework: string, ): Promise { return this.MapAbstractToTcpPortResult; } async getDebuggableApps( - deviceIdentifier: string + deviceIdentifier: string, ): Promise { return this.GetDebuggableAppsResult; } async getMappedAbstractToTcpPorts( deviceIdentifier: string, appIdentifiers: string[], - framework: string + framework: string, ): Promise> { return this.GetMappedAbstractToTcpPortsResult; } async getAppProcessId( deviceIdentifier: string, - appIdentifier: string + appIdentifier: string, ): Promise { while (this.GetAppProcessIdFailAttempts) { this.GetAppProcessIdFailAttempts--; @@ -181,7 +181,7 @@ export class AndroidProcessServiceStub return this.GetAppProcessIdResult; } async forwardFreeTcpToAbstractPort( - portForwardInputData: Mobile.IPortForwardData + portForwardInputData: Mobile.IPortForwardData, ): Promise { return this.ForwardFreeTcpToAbstractPortResult; } diff --git a/lib/common/test/with-done.ts b/lib/common/test/with-done.ts new file mode 100644 index 0000000000..b8bd29a8a2 --- /dev/null +++ b/lib/common/test/with-done.ts @@ -0,0 +1,15 @@ +export type DoneCallback = (err?: any) => void; + +/** + * Adapts a callback-style test to the promise form the runner expects. Useful + * for tests driven by an event emitter, where the assertion happens inside a + * listener rather than in the test body. + */ +export function withDone( + body: (done: DoneCallback) => void, +): () => Promise { + return () => + new Promise((resolve, reject) => { + body((err?: any) => (err ? reject(err) : resolve())); + }); +} diff --git a/lib/common/verify-node-version.ts b/lib/common/verify-node-version.ts index fa13e858ba..5a759c6478 100644 --- a/lib/common/verify-node-version.ts +++ b/lib/common/verify-node-version.ts @@ -2,6 +2,7 @@ import { color } from "../color"; import { ISystemWarning } from "./declarations"; +import { SystemWarningsSeverity } from "../definitions/system-warnings"; // Use only ES5 code here - pure JavaScript can be executed with any Node.js version (even 0.10, 0.12). /* tslint:disable:no-var-keyword no-var-requires prefer-const*/ diff --git a/lib/common/yok.ts b/lib/common/yok.ts index cf06c51445..13642ad417 100644 --- a/lib/common/yok.ts +++ b/lib/common/yok.ts @@ -1,32 +1,36 @@ import * as path from "path"; import * as _ from "lodash"; -import { annotate, isPromise } from "./helpers"; +import { isPromise } from "./helpers"; +import { reportDeprecation } from "./deprecation"; import { ERROR_NO_VALID_SUBCOMMAND_FORMAT } from "./constants"; import { CommandsDelimiters } from "./constants"; import { IDictionary } from "./declarations"; import { IInjector } from "./definitions/yok"; import { ICommandArgument, ICommand } from "./definitions/commands"; import { IKeyCommand, IValidKeyName } from "./definitions/key-commands"; - +import { Injector } from "./di/injector"; +import type { Provider } from "./di/providers"; +import { + CommandRegistry, + KeyCommandRegistry, + ModuleRegistry, + PublicApiBuilder, +} from "./contracts"; +import type { + DeferredCommandOptions, + DeferredCommandRejection, + DeferredCommandResult, +} from "./contracts"; + +/** + * The legacy global facade binding. New code should obtain the container via + * inject(Injector) inside an injection context rather than importing this; + * every legacy member on it is individually marked @deprecated. + */ export let injector: IInjector; -let indent = ""; -function trace(formatStr: string, ...args: any[]) { - // uncomment following lines when debugging dependency injection - // const items: any[] = []; - // for (let _i = 1; _i < arguments.length; _i++) { - // items[_i - 1] = arguments[_i]; - // } - // const util = require("util"); - // console.log(util.format.apply(util, [indent + formatStr].concat(args))); -} - -function pushIndent() { - indent += " "; -} - -function popIndent() { - indent = indent.slice(0, -2); +function rejected(rejection: DeferredCommandRejection): DeferredCommandResult { + return { registered: false, rejection }; } function forEachName(names: any, action: (name: string) => void): void { @@ -37,6 +41,9 @@ function forEachName(names: any, action: (name: string) => void): void { } } +/** + * @deprecated Yok-era class decorator with zero call sites; do not adopt. + */ export function register(...rest: any[]) { return function (target: any): void { // TODO: Check if 'rest' has more arguments that have to be registered @@ -44,6 +51,10 @@ export function register(...rest: any[]) { }; } +/** + * @deprecated Shape of Yok's old internal records; the container now keeps + * provider records in lib/common/di. + */ export interface IDependency { require?: string; resolver?: () => any; @@ -51,22 +62,57 @@ export interface IDependency { shared?: boolean; } -export class Yok implements IInjector { +/** + * The Yok facade IS the token-based `Injector` — it extends it — plus the + * legacy surface: command routing, the key-command namespace, the module + * loader, and the public-API builder. Those subsystems historically shared + * the container object and migrate out separately; until then they live here, + * individually marked @deprecated. + */ +export class Yok extends Injector implements IInjector { + /** + * @deprecated Escape hatch of the legacy require-time module map. + */ public overrideAlreadyRequiredModule: boolean = false; constructor() { + super(); this.register("injector", this); + // Each subsystem face resolves to the facade until it is physically + // extracted; extraction then swaps the provider without touching + // consumers of the token. + this.register([ + { provide: CommandRegistry, useValue: this }, + { provide: KeyCommandRegistry, useValue: this }, + { provide: ModuleRegistry, useValue: this }, + { provide: PublicApiBuilder, useValue: this }, + ]); } private COMMANDS_NAMESPACE: string = "commands"; + /** + * Parents whose dispatcher THIS instance synthesized. The require-ordering + * guard below must not fire for them: they exist because a child was + * registered, not because child requires ran out of order. + */ + private synthesizedParents = new Set(); + /** + * Parents whose record is only the placeholder requireCommand creates so a + * child's module can be loaded through the parent name. The dispatcher is + * meant to replace it once that module registers itself. + */ + private placeholderParents = new Set(); private KEY_COMMANDS_NAMESPACE: string = "keyCommands"; - private modules: { - [name: string]: IDependency; - } = {}; - - private resolutionProgress: any = {}; - private hierarchicalCommands: IDictionary = {}; - + // Keyed by command names, which extensions choose freely: a null prototype + // keeps a name like 'constructor' from reading back as an inherited member. + private hierarchicalCommands: IDictionary = Object.create(null); + /** Deferred command name -> the owner that claimed it first. */ + private deferredCommandOwners: IDictionary = Object.create(null); + + /** + * @deprecated Path-based command registration; use registerDeferredCommand, + * which routes without loading and reports conflicts structurally. + */ public requireCommand(names: any, file: string): void { forEachName(names, (commandName) => { const commands = commandName.split( @@ -76,7 +122,8 @@ export class Yok implements IInjector { if (commands.length > 1) { if ( _.startsWith(commands[1], "*") && - this.modules[this.createCommandName(commands[0])] + this.has(this.createCommandName(commands[0])) && + !this.synthesizedParents.has(commands[0]) ) { throw new Error( "Default commands should be required before child commands", @@ -96,30 +143,142 @@ export class Yok implements IInjector { if ( commands.length > 1 && - !this.modules[this.createCommandName(commands[0])] + !this.has(this.createCommandName(commands[0])) ) { + this.placeholderParents.add(commands[0]); this.require(this.createCommandName(commands[0]), file); if (commands[1] && !commandName.match(/\|\*/)) { this.require(this.createCommandName(commandName), file); } - } else { + } else if (!commandName.match(/\|\*/)) { + // Mirrors the default-command skip of the branch above: a default's + // own record comes from registerCommand, never from a require path. this.require(this.createCommandName(commandName), file); } }); } + public registerDeferredCommand( + name: string, + options: DeferredCommandOptions, + ): DeferredCommandResult { + if (name !== name.toLowerCase()) { + return rejected({ + reason: "invalid-name", + detail: + `command names are matched in lower case, so '${name}' can never ` + + `be dispatched; declare it as '${name.toLowerCase()}'`, + }); + } + + const claimedBy = this.deferredCommandOwners[name]; + if (claimedBy) { + return claimedBy === options.owner + ? { registered: true } + : rejected({ reason: "claimed", owner: claimedBy }); + } + + const commandRecordName = this.createCommandName(name); + if (this.has(commandRecordName)) { + return rejected( + this.synthesizedParents.has(name) + ? { reason: "subcommand-parent" } + : { reason: "built-in" }, + ); + } + + const commands = name.split(CommandsDelimiters.HierarchicalCommand); + const parentCommandName = commands.length > 1 ? commands[0] : null; + if ( + parentCommandName && + this.has(this.createCommandName(parentCommandName)) && + !this.synthesizedParents.has(parentCommandName) && + !this.placeholderParents.has(parentCommandName) + ) { + // Mirrors createHierarchicalCommand's refusal to overwrite a real + // command: no dispatcher gets created, so this name is unreachable. + return rejected({ + reason: "parent-is-command", + parent: parentCommandName, + }); + } + + super.register({ + provide: commandRecordName, + useLazyRequire: () => { + try { + options.load(); + } catch (err) { + throw new Error( + `Unable to load command '${name}' of ${options.owner} from ` + + `${options.source}: ${err.message}`, + ); + } + + if (!this.hasResolver(commandRecordName)) { + throw new Error( + `Command '${name}' of ${options.owner} was not registered when ` + + `${options.source} loaded. The module must export a ` + + `defineCommand() definition or register the command itself.`, + ); + } + }, + }); + this.deferredCommandOwners[name] = options.owner; + + if (parentCommandName) { + const subCommandName = _.tail(commands).join( + CommandsDelimiters.HierarchicalCommand, + ); + + if (!this.hierarchicalCommands[parentCommandName]) { + this.hierarchicalCommands[parentCommandName] = []; + } + + if ( + !_.includes( + this.hierarchicalCommands[parentCommandName], + subCommandName, + ) + ) { + this.hierarchicalCommands[parentCommandName].push(subCommandName); + } + + // The dispatcher routes off the recorded subcommand names alone, so + // reaching a sibling never loads this entry's module. + this.createHierarchicalCommand(parentCommandName, name); + } + + return { registered: true }; + } + + /** + * @deprecated Use provideLazy() from lib/common/di (via `Yok.di`) — the same + * deferred loading, token-based. + */ public require(names: any, file: string): void { forEachName(names, (name) => this.requireOne(name, file)); } + /** + * @deprecated Key-command counterpart of requireCommand; replaced together + * with the command registry. + */ public requireKeyCommand(name: any, file: string): void { this.requireOne(this.createKeyCommandName(name), file); } + /** + * @deprecated Backing store of the require('nativescript') surface. + * Do not add new entries through it. + */ public publicApi: any = { __modules__: {}, }; + /** + * @deprecated Legacy public-API builder. + */ public requirePublic(names: any, file: string): void { forEachName(names, (name) => { this.requireOne(name, file); @@ -127,6 +286,9 @@ export class Yok implements IInjector { }); } + /** + * @deprecated Legacy public-API builder. + */ public requirePublicClass(names: any, file: string): void { forEachName(names, (name) => { this.requireOne(name, file); @@ -152,7 +314,7 @@ export class Yok implements IInjector { } private resolveInstance(name: string): any { - let classInstance = _.first(this.modules[name].instances); + let classInstance = this.peek(name); if (!classInstance) { classInstance = this.resolve(name); } @@ -162,33 +324,64 @@ export class Yok implements IInjector { private requireOne(name: string, file: string): void { const relativePath = path.join("../", file); - const dependency: IDependency = { - require: require("fs").existsSync( - path.join(__dirname, relativePath + ".js"), - ) - ? relativePath - : file, - shared: true, - }; - - if (!this.modules[name] || this.overrideAlreadyRequiredModule) { - this.modules[name] = dependency; + const dependencyPath = require("fs").existsSync( + path.join(__dirname, relativePath + ".js"), + ) + ? relativePath + : file; + + if (!this.has(name) || this.overrideAlreadyRequiredModule) { + // Yok replaced the whole record on an allowed re-require, dropping any + // resolver and cached instances with it — preserved via remove(). + this.remove(name); + super.register({ + provide: name, + useLazyRequire: () => require(dependencyPath), + }); } else { throw new Error(`module '${name}' require'd twice.`); } } + /** + * @deprecated Slated for replacement by defineCommand and manifest-declared + * commands. + */ public registerCommand(names: any, resolver: any): void { forEachName(names, (name) => { const commands = name.split(CommandsDelimiters.HierarchicalCommand); this.register(this.createCommandName(name), resolver); if (commands.length > 1) { - this.createHierarchicalCommand(commands[0]); + const parentCommandName = commands[0]; + const subCommandName = _.tail(commands).join( + CommandsDelimiters.HierarchicalCommand, + ); + + if (!this.hierarchicalCommands[parentCommandName]) { + this.hierarchicalCommands[parentCommandName] = []; + } + + // Guarded: the legacy flow reaches here twice for one command — + // requireCommand records the subcommand, then the required module + // registers itself through this method. + if ( + !_.includes( + this.hierarchicalCommands[parentCommandName], + subCommandName, + ) + ) { + this.hierarchicalCommands[parentCommandName].push(subCommandName); + } + + this.createHierarchicalCommand(parentCommandName, name); } }); } + /** + * @deprecated Replaced together with the command registry. + */ public registerKeyCommand(name: IValidKeyName, resolver: IKeyCommand): void { this.register(this.createKeyCommandName(name), resolver); } @@ -204,6 +397,10 @@ export class Yok implements IInjector { return defaultCommand; } + /** + * @deprecated Hierarchical-routing internals of the legacy command registry; + * they move out of the container with the defineCommand work. + */ public buildHierarchicalCommand( parentCommandName: string, commandLineArguments: string[], @@ -243,7 +440,7 @@ export class Yok implements IInjector { .split(CommandsDelimiters.HierarchicalCommand) .map((command) => _.startsWith(command, CommandsDelimiters.DefaultCommandSymbol) - ? command.substr(1) + ? command.slice(1) : command, ), ); @@ -260,13 +457,34 @@ export class Yok implements IInjector { } } - private createHierarchicalCommand(name: string) { + private createHierarchicalCommand(name: string, triggeredBy?: string) { + if ( + this.has(this.createCommandName(name)) && + !this.synthesizedParents.has(name) && + !this.placeholderParents.has(name) + ) { + // Overwriting would make the registered command unreachable, which is + // strictly worse than leaving the subcommand unrouted. + const logger = this.get("logger", { optional: true }); + if (logger) { + logger.warn( + `'${name}' is already registered as a command of its own, so no ` + + `subcommand dispatcher was created for it${ + triggeredBy ? ` and '${triggeredBy}' cannot be reached` : "" + }. Rename one of the two.`, + ); + } + + return; + } + + this.synthesizedParents.add(name); const factory = () => { return { disableAnalytics: true, isHierarchicalCommand: true, execute: async (args: string[]): Promise => { - const commandsService = injector.resolve("commandsService"); + const commandsService = this.resolve("commandsService"); let commandName: string = null; const defaultCommand = this.getDefaultCommand(name, args); let commandArguments: ICommandArgument[] = []; @@ -320,7 +538,7 @@ export class Yok implements IInjector { }; }; - injector.registerCommand(name, factory); + this.registerCommand(name, factory); } private getHierarchicalCommandName( @@ -332,6 +550,10 @@ export class Yok implements IInjector { ); } + /** + * @deprecated Legacy command-registry routing. + * Side-effecting: fails with help output on a bad subcommand. + */ public async isValidHierarchicalCommand( commandName: string, commandArguments: string[], @@ -347,7 +569,7 @@ export class Yok implements IInjector { // In case buildHierarchicalCommand doesn't find a valid command // there isn't a valid command or default with those arguments - const errors = injector.resolve("errors"); + const errors = this.resolve("errors"); errors.failWithHelp(ERROR_NO_VALID_SUBCOMMAND_FORMAT, commandName); } @@ -358,6 +580,9 @@ export class Yok implements IInjector { return false; } + /** + * @deprecated Legacy command-registry routing. + */ public isDefaultCommand(commandName: string): boolean { return ( commandName.indexOf(CommandsDelimiters.DefaultCommandSymbol) > 0 && @@ -365,31 +590,56 @@ export class Yok implements IInjector { ); } - public register(name: string, resolver: any, shared?: boolean): void { - shared = shared === undefined ? true : shared; - trace("registered '%s'", name); - - const dependency: any = this.modules[name] || {}; - dependency.shared = shared; + /** + * @deprecated Legacy name-based registration. Use a Provider (the overload + * below) or provide(); a contract's token name keeps string spellings + * resolvable. + */ + public register(name: string, resolver: any, shared?: boolean): void; + public register(providers: Provider | Provider[]): void; + public register( + nameOrProviders: string | Provider | Provider[], + resolver?: any, + shared?: boolean, + ): void { + if (typeof nameOrProviders !== "string") { + super.register(nameOrProviders); + return; + } + shared = shared === undefined ? true : shared; if (_.isFunction(resolver)) { - dependency.resolver = resolver; - } else { - dependency.instances = dependency.instances || []; - if (shared) { - dependency.instances[0] = resolver; - } else { - dependency.instances.push(resolver); + if (resolver.length === 0 && !resolver.prototype) { + // A prototype-less zero-parameter function (an arrow factory) cannot + // be `new`ed and has no parameters to resolve, so annotate() would + // contribute nothing — register it as a plain factory. + super.register({ + provide: nameOrProviders, + useFactory: <() => any>resolver, + shared, + }); + return; } - } - this.modules[name] = dependency; + // Classes and factory functions alike: the legacy provider kind + // annotate()s the resolver and calls or news it by casing. + super.register({ + provide: nameOrProviders, + useLegacyClass: resolver, + shared, + }); + } else { + super.register({ provide: nameOrProviders, useValue: resolver, shared }); + } } + /** + * @deprecated Legacy command-registry lookup. + */ public resolveCommand(name: string): ICommand { let command: ICommand; const commandModuleName = this.createCommandName(name); - if (!this.modules[commandModuleName]) { + if (!this.has(commandModuleName)) { return null; } command = this.resolve(commandModuleName); @@ -397,10 +647,13 @@ export class Yok implements IInjector { return command; } + /** + * @deprecated Legacy command-registry lookup. + */ public resolveKeyCommand(name: string): IKeyCommand { let command: IKeyCommand; const commandModuleName = this.createKeyCommandName(name); - if (!this.modules[commandModuleName]) { + if (!this.has(commandModuleName)) { return null; } @@ -409,12 +662,17 @@ export class Yok implements IInjector { return command; } + /** + * @deprecated Use inject(Token) in an injection context, or Injector.get / + * createInstance from lib/common/di (via `Yok.di`). + */ public resolve(param: any, ctorArguments?: IDictionary): any { if (_.isFunction(param)) { - return this.resolveConstructor(param, ctorArguments); - } else { - return this.resolveByName(param, ctorArguments); + // By-class resolution is transient and never retained — Yok did not + // track these instances for disposal either. + return this.createInstance(param, [], ctorArguments); } + return this.getWithLegacyArguments(param, ctorArguments); } /* Regex to match dynamic calls in the following format: @@ -423,11 +681,20 @@ export class Yok implements IInjector { #{moduleName.functionName(param1, param2)} - multiple parameters separated with comma are supported Check dynamicCall method for sample usage of this regular expression and see how to determine the passed parameters */ + /** + * @deprecated String-reflective help templating; removable only together with + * the help-template pipeline. Usage is + * runtime-traced via reportDeprecation. + */ public get dynamicCallRegex(): RegExp { return /#{([^.]+)\.([^}]+?)(\((.+)\))*}/; } + /** + * @deprecated See dynamicCallRegex. + */ public getDynamicCallData(call: string, args?: any[]): any { + reportDeprecation({ api: "injector.dynamicCall", detail: call }); const parsed = call.match(this.dynamicCallRegex); const module = this.resolve(parsed[1]); if (!args && parsed[3]) { @@ -437,6 +704,9 @@ export class Yok implements IInjector { return module[parsed[2]].apply(module, args); } + /** + * @deprecated See dynamicCallRegex. + */ public async dynamicCall(call: string, args?: any[]): Promise { const data = this.getDynamicCallData(call, args); @@ -447,93 +717,16 @@ export class Yok implements IInjector { return data; } - private resolveConstructor( - ctor: any, - ctorArguments?: { [key: string]: any }, - ): any { - annotate(ctor); - - const resolvedArgs = ctor.$inject.args.map((paramName: any) => { - if (ctorArguments && ctorArguments.hasOwnProperty(paramName)) { - return ctorArguments[paramName]; - } else { - return this.resolve(paramName); - } - }); - - const name = ctor.$inject.name; - if (name && name[0] === name[0].toUpperCase()) { - return new (ctor)(...resolvedArgs); - } else { - return ctor.apply(null, resolvedArgs); - } - } - - private resolveByName(name: string, ctorArguments?: IDictionary): any { - if (name[0] === "$") { - name = name.substr(1); - } - - if (this.resolutionProgress[name]) { - throw new Error(`Cyclic dependency detected on dependency '${name}'`); - } - this.resolutionProgress[name] = true; - - trace("resolving '%s'", name); - pushIndent(); - - let dependency: IDependency; - let instance: any; - try { - dependency = this.resolveDependency(name); - - if (!dependency) { - throw new Error("unable to resolve " + name); - } - - if ( - !dependency.instances || - !dependency.instances.length || - !dependency.shared - ) { - if (!dependency.resolver) { - throw new Error("no resolver registered for " + name); - } - - dependency.instances = dependency.instances || []; - - instance = this.resolveConstructor(dependency.resolver, ctorArguments); - dependency.instances.push(instance); - } else { - instance = _.first(dependency.instances); - } - } finally { - popIndent(); - delete this.resolutionProgress[name]; - } - - return instance; - } - - private resolveDependency(name: string): IDependency { - const module = this.modules[name]; - if (!module) { - throw new Error("unable to resolve " + name); - } - - if (module.require) { - require(module.require); - } - return module; - } - + /** + * @deprecated Legacy command-registry enumeration; feeds shell autocompletion + * and help, so the `|` encoding is user-visible. + */ public getRegisteredCommandsNames(includeDev: boolean): string[] { - const modulesNames: string[] = _.keys(this.modules); - const commandsNames: string[] = _.filter(modulesNames, (moduleName) => - _.startsWith(moduleName, `${this.COMMANDS_NAMESPACE}.`), + const commandsNames = this.getRegisteredNames( + `${this.COMMANDS_NAMESPACE}.`, ); let commands = _.map(commandsNames, (commandName: string) => - commandName.substr(this.COMMANDS_NAMESPACE.length + 1), + commandName.slice(this.COMMANDS_NAMESPACE.length + 1), ); if (!includeDev) { commands = _.reject(commands, (command) => _.startsWith(command, "dev-")); @@ -541,17 +734,22 @@ export class Yok implements IInjector { return commands; } + /** + * @deprecated Legacy command-registry enumeration. + */ public getRegisteredKeyCommandsNames(): string[] { - const modulesNames: string[] = _.keys(this.modules); - const commandsNames: string[] = _.filter(modulesNames, (moduleName) => - _.startsWith(moduleName, `${this.KEY_COMMANDS_NAMESPACE}.`), + const commandsNames = this.getRegisteredNames( + `${this.KEY_COMMANDS_NAMESPACE}.`, ); - let commands = _.map(commandsNames, (commandName: string) => - commandName.substr(this.KEY_COMMANDS_NAMESPACE.length + 1), + const commands = _.map(commandsNames, (commandName: string) => + commandName.slice(this.KEY_COMMANDS_NAMESPACE.length + 1), ); return commands; } + /** + * @deprecated Legacy command-registry routing. + */ public getChildrenCommandsNames(commandName: string): string[] { return this.hierarchicalCommands[commandName]; } @@ -564,24 +762,45 @@ export class Yok implements IInjector { return `${this.KEY_COMMANDS_NAMESPACE}.${name}`; } - public dispose(): void { - Object.keys(this.modules).forEach((moduleName) => { - const instances = this.modules[moduleName].instances; - _.forEach(instances, (instance) => { - if (instance && instance.dispose && instance !== this) { - instance.dispose(); - } - }); - }); + /** + * @deprecated Delegates to Injector.dispose (reverse instantiation order); + * new code disposes the di container directly. + */ + public dispose(exclude: any[] = []): void { + super.dispose([this, ...exclude]); } } -if (!(global).$injector) { - (global).$injector = new Yok(); - injector = (global).$injector; +// The global is the published legacy surface. It is an accessor pair so a +// direct `global.$injector = x` assignment — allowed for third parties — +// stays synchronized with the module binding that getInjector() and internal +// code read; a plain data property would silently fork the two. +injector = (global).$injector || new Yok(); +Object.defineProperty(global, "$injector", { + get: () => injector, + set: (value: IInjector) => { + injector = value; + }, + configurable: true, +}); + +/** + * Accessor for the process-wide facade, for code that cannot receive the + * injector through DI or a static import (import cycles, decorator bodies). + * Prefer inject(Injector) in an injection context; prefer a constructor + * dependency in services. Never read global.$injector directly — the global + * exists only as the published legacy surface for extensions and hooks. + */ +export function getInjector(): IInjector { + return injector; } +/** + * @deprecated Global-singleton wiring for the legacy facade; new code receives + * the container via inject(Injector) instead of a process-wide global. + */ export function setGlobalInjector(inj: IInjector): IInjector { - (global).$injector = injector = inj; + injector = inj; + (global).$injector = inj; return inj; } diff --git a/lib/config.ts b/lib/config.ts index 16291dfadb..86944af5b8 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -16,7 +16,8 @@ export class Configuration implements IConfiguration { DEBUG = false; ANDROID_DEBUG_UI: string = null; USE_POD_SANDBOX: boolean = false; - GA_TRACKING_ID: string = null; + GA_MEASUREMENT_ID: string = null; + GA_API_SECRET: string = null; DISABLE_HOOKS: boolean = false; /*don't require logger and everything that has logger as dependency in config.js due to cyclic dependency*/ @@ -122,7 +123,7 @@ export class StaticConfig implements IStaticConfig { ["version"], "exit", undefined, - { throwError: false } + { throwError: false }, ); if (proc.stderr) { @@ -160,7 +161,7 @@ export class StaticConfig implements IStaticConfig { "resources", "platform-tools", "android", - process.platform + process.platform, ); const pathToPackageJson = path.join(__dirname, "..", "package.json"); const nsCliVersion = require(pathToPackageJson).version; diff --git a/lib/constants.ts b/lib/constants.ts index c76d6f6696..7420893fab 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -17,6 +17,12 @@ export const TNS_CORE_THEME_NAME = "nativescript-theme-core"; export const SCOPED_TNS_CORE_THEME_NAME = "@nativescript/theme"; export const WEBPACK_PLUGIN_NAME = "@nativescript/webpack"; export const RSPACK_PLUGIN_NAME = "@nativescript/rspack"; +// Root of the project-relative directory the Vite bundler writes its build +// output to before the CLI copies it into the platforms app folder. The CLI +// stages each platform in its own subdirectory (`.ns-vite-build/`) +// and tells `@nativescript/vite` where via `NS_VITE_DIST_DIR`; the package's +// own fallback (`.ns-vite-build`) only applies to standalone `vite` runs. +export const VITE_DIST_FOLDER_NAME = ".ns-vite-build"; export const TNS_CORE_MODULES_WIDGETS_NAME = "tns-core-modules-widgets"; export const UI_MOBILE_BASE_NAME = "@nativescript/ui-mobile-base"; export const TNS_ANDROID_RUNTIME_NAME = "tns-android"; @@ -30,7 +36,7 @@ export const ANDROID_DEVICE_APP_ROOT_TEMPLATE = `/data/data/%s/files`; export const NODE_MODULE_CACHE_PATH_KEY_NAME = "node-modules-cache-path"; export const DEFAULT_APP_IDENTIFIER_PREFIX = "org.nativescript"; export const LIVESYNC_EXCLUDED_DIRECTORIES = ["app_resources"]; -export const TESTING_FRAMEWORKS = ["jasmine", "mocha", "qunit"]; +export const TESTING_FRAMEWORKS = ["vitest", "jasmine", "mocha", "qunit"]; export const TEST_RUNNER_NAME = "@nativescript/unit-test-runner"; export const LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"]; export const XML_FILE_EXTENSION = ".xml"; @@ -61,6 +67,7 @@ export const BUNDLE_DIR = "bundle"; export const RESOURCES_DIR = "res"; export const CONFIG_NS_FILE_NAME = "nsconfig.json"; export const CONFIG_NS_APP_RESOURCES_ENTRY = "appResourcesPath"; +export const CONFIG_NS_BUILD_ENTRY = "buildPath"; export const CONFIG_NS_APP_ENTRY = "appPath"; export const CONFIG_FILE_NAME_DISPLAY = "nativescript.config.(js|ts)"; export const CONFIG_FILE_NAME_JS = "nativescript.config.js"; @@ -172,9 +179,7 @@ export class ITMSConstants { static altoolExecutableName = "altool"; } -class ItunesConnectApplicationTypesClass - implements IiTunesConnectApplicationType -{ +class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationType { public iOS = "iOS App"; public Mac = "Mac OS X App"; } @@ -214,6 +219,15 @@ export const DEBUGGER_ATTACHED_EVENT_NAME = "debuggerAttached"; export const DEBUGGER_DETACHED_EVENT_NAME = "debuggerDetached"; export const VERSION_STRING = "version"; export const INSPECTOR_CACHE_DIRNAME = "ios-inspector"; +export const BUNDLETOOL_CACHE_DIRNAME = "bundletool"; +export const BUNDLETOOL_VERSION = "1.18.2"; +// sha256 of bundletool-all-.jar as published on GitHub; +// must be updated together with BUNDLETOOL_VERSION or the download is rejected +export const BUNDLETOOL_SHA256 = + "378b5434cd1378bef6b2bc527b8c7f0ff2584b273830335bce54d6d0813c8584"; +export const BUNDLETOOL_RELEASES_URL = + "https://github.com/google/bundletool/releases/download"; +export const BUNDLETOOL_PATH_ENV_VAR = "NS_BUNDLETOOL_PATH"; export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; const ANDROID_SIGNING_REQUIRED_MESSAGE = "you need to specify all --key-store-* options."; @@ -351,9 +365,7 @@ export const enum PlatformTypes { } export type SupportedPlatform = - | PlatformTypes.ios - | PlatformTypes.android - | PlatformTypes.visionos; + PlatformTypes.ios | PlatformTypes.android | PlatformTypes.visionos; export const PODFILE_NAME = "Podfile"; @@ -409,6 +421,7 @@ export enum IOSNativeTargetTypes { watchApp = "watch_app", watchExtension = "watch_extension", appExtension = "app_extension", + application = "application", } const pathToLoggerAppendersDir = join( @@ -502,3 +515,8 @@ export enum PackageManagers { yarn2 = "yarn2", bun = "bun", } + +export enum BuildNames { + debug = "Debug", + release = "Release", +} diff --git a/lib/contracts/child-process.ts b/lib/contracts/child-process.ts new file mode 100644 index 0000000000..96b2a099bf --- /dev/null +++ b/lib/contracts/child-process.ts @@ -0,0 +1,78 @@ +import { Contract } from "../common/di/contract"; +import type * as child_process from "child_process"; +import type { + IExecOptions, + ISpawnFromEventOptions, + ISpawnResult, +} from "../common/declarations"; + +/** + * Promise-based wrapper around Node's `child_process` module. + */ +@Contract({ name: "childProcess" }) +export abstract class ChildProcess { + abstract exec( + command: string, + options?: any, + execOptions?: IExecOptions, + ): Promise; + + abstract execFile(command: string, args: string[]): Promise; + + abstract spawn( + command: string, + args?: string[], + options?: any, + ): child_process.ChildProcess; + + abstract spawnFromEvent( + command: string, + args: string[], + event: string, + options?: any, + spawnFromEventOptions?: ISpawnFromEventOptions, + ): Promise; + + abstract trySpawnFromCloseEvent( + command: string, + args: string[], + options?: any, + spawnFromEventOptions?: ISpawnFromEventOptions, + ): Promise; + + abstract tryExecuteApplication( + command: string, + args: string[], + event: string, + errorMessage: string, + condition?: (childProcess: any) => boolean, + ): Promise; + + /** + * This is a special case of the child_process.spawn() functionality for spawning Node.js processes. + * In addition to having all the methods in a normal ChildProcess instance, the returned object has a communication channel built-in. + * Note: Unlike the fork() POSIX system call, child_process.fork() does not clone the current process. + * @param {string} modulePath String The module to run in the child + * @param {string[]} args Array List of string arguments You can access them in the child with 'process.argv'. + * @param {string} options Object + * @return {child_process} ChildProcess object. + */ + abstract fork( + modulePath: string, + args?: string[], + options?: { + cwd?: string; + env?: any; + execPath?: string; + execArgv?: string[]; + silent?: boolean; + uid?: number; + gid?: number; + }, + ): any; +} + +// The event-emitter surface is merged in rather than inherited: `extends +// EventEmitter` would need a runtime import, and everything reachable from +// lib/contracts must stay side-effect-free. +export interface ChildProcess extends NodeJS.EventEmitter {} diff --git a/lib/contracts/devices-service.ts b/lib/contracts/devices-service.ts new file mode 100644 index 0000000000..782ff5b4df --- /dev/null +++ b/lib/contracts/devices-service.ts @@ -0,0 +1,176 @@ +import { Contract } from "../common/di/contract"; +import type { IAppInstalledInfo } from "../common/declarations"; + +/** + * The EventEmitter surface is merged in rather than redeclared as abstract + * members: the contract must stay free of runtime imports, so it cannot extend + * `EventEmitter`, and merging keeps the signatures (including the `this` + * returns) tied to the ambient definition instead of a hand-copied snapshot. + */ +export interface DevicesService extends NodeJS.EventEmitter {} + +/** + * Discovers connected devices and emulators and executes actions against them. + */ +@Contract({ name: "devicesService" }) +export abstract class DevicesService { + /** The platform the service has been initialized for. */ + abstract platform: string; + + /** Whether any device matching the current initialization options is attached. */ + abstract hasDevices: boolean; + + /** The number of devices matching the current initialization options. */ + abstract deviceCount: number; + + abstract execute( + action: (device: Mobile.IDevice) => Promise, + canExecute?: (dev: Mobile.IDevice) => boolean, + options?: { allowNoDevices?: boolean }, + ): Promise[]>; + + /** + * Initializes DevicesService, so after that device operations could be executed. + * @param {IDevicesServicesInitializationOptions} data Defines the options which will be used for whole devicesService. + * @return {Promise} + */ + abstract initialize( + data?: Mobile.IDevicesServicesInitializationOptions, + ): Promise; + + /** + * Add an IDeviceDiscovery instance which will from now on report devices. The instance should implement IDeviceDiscovery and raise "deviceFound" and "deviceLost" events. + * @param {IDeviceDiscovery} deviceDiscovery Instance, implementing IDeviceDiscovery and raising raise "deviceFound" and "deviceLost" events. + * @return {void} + */ + abstract addDeviceDiscovery(deviceDiscovery: Mobile.IDeviceDiscovery): void; + + abstract getDevices(): Mobile.IDeviceInfo[]; + + /** + * Gets device instance by specified identifier or number. + * @param {string} deviceOption The specified device identifier or number. + * @returns {Promise} Instance of IDevice. + */ + abstract getDevice(deviceOption: string): Promise; + + abstract getDevicesForPlatform(platform: string): Mobile.IDevice[]; + + abstract getDeviceInstances(): Mobile.IDevice[]; + + abstract getDeviceByDeviceOption(): Mobile.IDevice; + + abstract isAndroidDevice(device: Mobile.IDevice): boolean; + + abstract isiOSDevice(device: Mobile.IDevice): boolean; + + abstract isiOSSimulator(device: Mobile.IDevice): boolean; + + abstract isOnlyiOSSimultorRunning(): boolean; + + abstract isAppInstalledOnDevices( + deviceIdentifiers: string[], + appIdentifier: string, + framework: string, + projectDir: string, + ): Promise[]; + + abstract setLogLevel(logLevel: string, deviceIdentifier?: string): void; + + abstract deployOnDevices( + deviceIdentifiers: string[], + packageFile: string, + packageName: string, + framework: string, + projectDir: string, + ): Promise[]; + + abstract getDeviceByIdentifier(identifier: string): Mobile.IDevice; + + abstract mapAbstractToTcpPort( + deviceIdentifier: string, + appIdentifier: string, + framework: string, + ): Promise; + + abstract getDebuggableApps( + deviceIdentifiers: string[], + ): Promise[]; + + abstract getDebuggableViews( + deviceIdentifier: string, + appIdentifier: string, + ): Promise; + + /** + * Returns all applications installed on the specified device. + * @param {string} deviceIdentifer The identifier of the device for which to get installed applications. + * @returns {Promise} Array of all application identifiers of the apps installed on device. + */ + abstract getInstalledApplications( + deviceIdentifier: string, + ): Promise; + + /** + * Returns all available iOS and/or Android emulators. + * @param options The options that can be passed to filter the result. + * @returns {Promise} Dictionary with the following format: { ios: { devices: Mobile.IDeviceInfo[], errors: string[] }, android: { devices: Mobile.IDeviceInfo[], errors: string[]}}. + */ + abstract getEmulatorImages( + options?: Mobile.IListEmulatorsOptions, + ): Promise; + + /** + * Starts an emulator by provided options. + * @param options + * @returns {Promise} - Returns array of errors. + */ + abstract startEmulator( + options?: Mobile.IStartEmulatorOptions, + ): Promise; + + /** + * Starts polling for attached devices, raising the deviceFound/deviceLost + * events as the set of attached devices changes. Calling it while a poll + * is already running is a no-op. + * @param {Mobile.IDeviceLookingOptions} deviceInitOpts Options describing which devices to look for and how often to poll. + * @returns {void} + */ + abstract startDeviceDetectionInterval( + deviceInitOpts?: Mobile.IDeviceLookingOptions, + ): void; + + /** + * Stops the poll started by startDeviceDetectionInterval. + * @returns {void} + */ + abstract stopDeviceDetectionInterval(): void; + + /** + * Starts polling for available emulator images, raising the + * emulatorImageFound/emulatorImageLost events as the set changes. + * @param {Mobile.IHasDetectionInterval} opts Options describing how often to poll. + * @returns {void} + */ + abstract startEmulatorDetectionInterval( + opts?: Mobile.IHasDetectionInterval, + ): void; + + /** + * Stops the poll started by startEmulatorDetectionInterval. + * @returns {void} + */ + abstract stopEmulatorDetectionInterval(): void; + + /** + * Returns a single device based on the specified options. If more than one devices are matching, + * prompts the user for a manual choice or returns the first one for non interactive terminals. + */ + abstract pickSingleDevice( + options: Mobile.IPickSingleDeviceOptions, + ): Promise; + + abstract getPlatformsFromDeviceDescriptors( + deviceDescriptors: ILiveSyncDeviceDescriptor[], + ): string[]; +} diff --git a/lib/contracts/doctor-service.ts b/lib/contracts/doctor-service.ts new file mode 100644 index 0000000000..9a64fbd7a4 --- /dev/null +++ b/lib/contracts/doctor-service.ts @@ -0,0 +1,38 @@ +import { Contract } from "../common/di/contract"; +import type { ISpawnResult } from "../common/declarations"; +import type { IOptions } from "../declarations"; + +/** + * Verifies the host OS configuration — the code behind `ns doctor`. + */ +@Contract({ name: "doctorService" }) +export abstract class DoctorService { + /** + * Verifies the host OS configuration and prints warnings to the users. + * @param configOptions Defines if the result should be tracked by Analytics. + */ + abstract printWarnings(configOptions?: { + trackResult?: boolean; + projectDir?: string; + runtimeVersion?: string; + options?: IOptions; + forceCheck?: boolean; + platform?: string; + }): Promise; + + /** Runs the setup script on the host machine. */ + abstract runSetupScript(): Promise; + + /** + * Checks whether the environment is properly configured for local builds. + */ + abstract canExecuteLocalBuild(configuration?: { + platform?: string; + projectDir?: string; + runtimeVersion?: string; + forceCheck?: boolean; + }): Promise; + + /** Checks and notifies users of deprecated short imports in their app. */ + abstract checkForDeprecatedShortImportsInAppDir(projectDir: string): void; +} diff --git a/lib/contracts/errors.ts b/lib/contracts/errors.ts new file mode 100644 index 0000000000..215b750d5b --- /dev/null +++ b/lib/contracts/errors.ts @@ -0,0 +1,33 @@ +import { Contract } from "../common/di/contract"; +import type { IFailOptions } from "../common/declarations"; + +/** + * Raises CLI failures and wraps command execution so they are reported and + * turned into a process exit code. + */ +@Contract({ name: "errors" }) +export abstract class Errors { + abstract fail(formatStr: string, ...args: any[]): never; + abstract fail(opts: IFailOptions, ...args: any[]): never; + + /** + * @deprecated use `fail` instead + */ + abstract failWithoutHelp(message: string, ...args: any[]): never; + /** + * @deprecated use `fail` instead + */ + abstract failWithoutHelp(opts: IFailOptions, ...args: any[]): never; + + abstract failWithHelp(formatStr: string, ...args: any[]): never; + abstract failWithHelp(opts: IFailOptions, ...args: any[]): never; + + abstract beginCommand( + action: () => Promise, + printCommandHelp: () => Promise, + ): Promise; + + abstract verifyHeap(message: string): void; + + abstract printCallStack: boolean; +} diff --git a/lib/contracts/file-system.ts b/lib/contracts/file-system.ts new file mode 100644 index 0000000000..787f9b2383 --- /dev/null +++ b/lib/contracts/file-system.ts @@ -0,0 +1,337 @@ +import { Contract } from "../common/di/contract"; +import type { IFsStats, IReadFileOptions } from "../common/declarations"; + +/** + * Wraps the host file system — reads, writes, directory traversal and archive + * handling used throughout the CLI. + */ +@Contract({ name: "fs" }) +export abstract class FileSystem { + abstract zipFiles( + zipFile: string, + files: string[], + zipPathCallback: (path: string) => string, + ): Promise; + + abstract unzip( + zipFile: string, + destinationDir: string, + options?: { overwriteExisitingFiles?: boolean; caseSensitive?: boolean }, + fileFilters?: string[], + ): Promise; + + /** + * Test whether or not the given path exists by checking with the file system. + * @param {string} path Path to be checked. + * @returns {boolean} True if path exists, false otherwise. + */ + abstract exists(path: string): boolean; + + /** + * Deletes a file. + * @param {string} path Path to be deleted. + * @returns {void} undefined + */ + abstract deleteFile(path: string): void; + + /** + * Deletes whole directory. + * @param {string} directory Path to directory that has to be deleted. + * @returns {void} + */ + abstract deleteDirectory(directory: string): void; + + /** + * Deletes whole directory without throwing exceptions. + * @param {string} directory Path to directory that has to be deleted. + * @returns {void} + */ + abstract deleteDirectorySafe(directory: string): void; + + /** + * Returns the size of specified file. + * @param {string} path Path to file. + * @returns {number} File size in bytes. + */ + abstract getFileSize(path: string): number; + + /** + * Returns the size of specified path (recurses into all sub-directories if the path is a directory). + * @param {string} path Path to file or directory. + * @returns {number} File size in bytes. + */ + abstract getSize(path: string): number; + + /** + * Change file timestamps of the file referenced by the supplied path. + * @param {string} path File path + * @param {Date} atime Access time + * @param {Date} mtime Modified time + * @returns {void} + */ + abstract utimes(path: string, atime: Date, mtime: Date): void; + + abstract futureFromEvent( + eventEmitter: NodeJS.EventEmitter, + event: string, + ): Promise; + + /** + * Create a new directory and any necessary subdirectories at specified location. + * @param {string} path Directory to be created. + * @returns {void} + */ + abstract createDirectory(path: string): void; + + /** + * Reads contents of directory and returns an array of filenames excluding '.' and '..'. + * @param {string} path Path to directory to be checked. + * @retruns {string[]} Array of filenames excluding '.' and '..' + */ + abstract readDirectory(path: string): string[]; + + /** + * Reads the entire contents of a file. + * @param {string} filename Path to the file that has to be read. + * @param {string} options Options used for reading the file - encoding and flags. + * @returns {string|Buffer} Content of the file as buffer. In case encoding is specified, the content is returned as string. + */ + abstract readFile( + filename: string, + options?: IReadFileOptions, + ): string | Buffer; + + /** + * Reads the entire contents of a file and returns the result as string. + * @param {string} filename Path to the file that has to be read. + * @param {IReadFileOptions | string} encoding Options used for reading the file - encoding and flags. If options are not passed, utf8 is used. + * @returns {string} Content of the file as string. + */ + abstract readText( + filename: string, + encoding?: IReadFileOptions | string, + ): string; + + /** + * Reads the entire content of a file and parses it to JSON object. + * @param {string} filename Path to the file that has to be read. + * @param {string} encoding File encoding, defaults to utf8. + * @returns {string} Content of the file as JSON object. + */ + abstract readJson(filename: string, encoding?: string): any; + + abstract readStdin(): Promise; + + /** + * Writes data to a file, replacing the file if it already exists. data can be a string or a buffer. + * @param {string} filename Path to file to be created. + * @param {string | Buffer} data Data to be written to file. + * @param {string} encoding @optional File encoding, defaults to utf8. + * @returns {void} + */ + abstract writeFile( + filename: string, + data: string | Buffer, + encoding?: string, + ): void; + + /** + * Appends data to a file, creating the file if it does not yet exist. Data can be a string or a buffer. + * @param {string} filename Path to file to be created. + * @param {string | Buffer} data Data to be appended to file. + * @param {string} encoding @optional File encoding, defaults to utf8. + * @returns {void} + */ + abstract appendFile( + filename: string, + data: string | Buffer, + encoding?: string, + ): void; + + /** + * Writes JSON data to file. + * @param {string} filename Path to file to be created. + * @param {any} data JSON data to be written to file. + * @param {string} space Identation that will be used for the file. + * @param {string} encoding @optional File encoding, defaults to utf8. + * @returns {void} + */ + abstract writeJson( + filename: string, + data: any, + space?: string, + encoding?: string, + ): void; + + /** + * Copies a file. + * @param {string} sourceFileName The original file that has to be copied. + * @param {string} destinationFileName The filepath where the file should be copied. + * @returns {void} + */ + abstract copyFile(sourceFileName: string, destinationFileName: string): void; + + /** + * Returns unique file name based on the passed name by checkin if it exists and adding numbers to the passed name until a non-existent file is found. + * @param {string} baseName The name based on which the unique name will be generated. + * @returns {string} Unique filename. In case baseName does not exist, it will be returned. + */ + abstract getUniqueFileName(baseName: string): string; + + /** + * Checks if specified directory is empty. + * @param {string} directoryPath The directory that will be checked. + * @returns {boolean} True in case the directory is empty. False otherwise. + */ + abstract isEmptyDir(directoryPath: string): boolean; + + abstract isRelativePath(path: string): boolean; + + /** + * Checks if directory exists and if not - creates it. + * @param {string} directoryPath Directory path. + * @returns {void} + */ + abstract ensureDirectoryExists(directoryPath: string): void; + + /** + * Renames file/directory. This method throws error in case the original file name does not exist. + * @param {string} oldPath The original filename. + * @param {string} newPath New filename. + * @returns {string} void. + */ + abstract rename(oldPath: string, newPath: string): void; + + /** + * Renames specified file to the specified name only in case it exists. + * Used to skip ENOENT errors when rename is called directly. + * @param {string} oldPath Path to original file that has to be renamed. If this file does not exists, no operation is executed. + * @param {string} newPath The path where the file will be moved. + * @return {boolean} True in case of successful rename. False in case the file does not exist. + */ + abstract renameIfExists(oldPath: string, newPath: string): boolean; + + /** + * Returns information about the specified file. + * In case the passed path is symlink, the returned information is about the original file. + * @param {string} path Path to file for which the information will be taken. + * @returns {IFsStats} Inforamation about the specified file. + */ + abstract getFsStats(path: string): IFsStats; + + /** + * Returns information about the specified file. + * In case the passed path is symlink, the returned information is about the symlink itself. + * @param {string} path Path to file for which the information will be taken. + * @returns {IFsStats} Inforamation about the specified file. + */ + abstract getLsStats(path: string): IFsStats; + + abstract symlink( + sourcePath: string, + destinationPath: string, + type: "file", + ): void; + abstract symlink( + sourcePath: string, + destinationPath: string, + type: "dir", + ): void; + abstract symlink( + sourcePath: string, + destinationPath: string, + type: "junction", + ): void; + /** + * Creates a symbolic link. + * Symbolic links are interpreted at run time as if the contents of the + * link had been substituted into the path being followed to find a file + * or directory. + * @param {string} sourcePath The original path of the file/dir. + * @param {string} destinationPath The destination where symlink will be created. + * @param {string} type "file", "dir" or "junction". Default is 'file'. + * Type option is only available on Windows (ignored on other platforms). + * Note that Windows junction points require the destination path to be absolute. + * When using 'junction', the target argument will automatically be normalized to absolute path. + * @returns {void} + */ + abstract symlink( + sourcePath: string, + destinationPath: string, + type?: string, + ): void; + + abstract createReadStream( + path: string, + options?: { + flags?: string; + encoding?: string; + fd?: number; + mode?: number; + bufferSize?: number; + start?: number; + end?: number; + highWaterMark?: number; + }, + ): NodeJS.ReadableStream; + + abstract createWriteStream( + path: string, + options?: { + flags?: string; + encoding?: string; + string?: string; + }, + ): any; + + /** + * Changes file mode of the specified file. In case it is a symlink, the original file's mode is modified. + * @param {string} path Filepath to be modified. + * @param {number | string} mode File mode. + * @returns {void} + */ + abstract chmod(path: string, mode: number | string): void; + + abstract setCurrentUserAsOwner(path: string, owner: string): Promise; + + abstract enumerateFilesInDirectorySync( + directoryPath: string, + filterCallback?: (file: string, stat: IFsStats) => boolean, + opts?: { + enumerateDirectories?: boolean; + includeEmptyDirectories?: boolean; + }, + ): string[]; + + /** + * Hashes a file's contents. + * @param {string} fileName Path to file + * @param {Object} options algorithm and digest encoding. Default values are sha1 for algorithm and hex for encoding + * @return {Promise} The computed shasum + */ + abstract getFileShasum( + fileName: string, + options?: { algorithm?: string; encoding?: "hex" | "base64" }, + ): Promise; + + /** + * @param {string} options Options, can be undefined or a combination of "-r" (recursive) and "-f" (force) + * @param {string[]} files files and direcories to delete + */ + abstract rm(options: string, ...files: string[]): void; + + /** + * Deletes all empty parent directories. + * @param {string} directory The directory from which this method will start looking for empty parents. + * @returns {void} + */ + abstract deleteEmptyParents(directory: string): void; + + /** + * Return the canonicalized absolute pathname. + * NOTE: The method accepts second argument, but it's type and usage is different in Node 4 and Node 6. Once we drop support for Node 4, we can use the second argument as well. + * @param {string} filePath Path to file which should be resolved. + * @returns {string} The canonicalized absolute path to file. + */ + abstract realpath(filePath: string): string; +} diff --git a/lib/contracts/host-info.ts b/lib/contracts/host-info.ts new file mode 100644 index 0000000000..b31bfa5477 --- /dev/null +++ b/lib/contracts/host-info.ts @@ -0,0 +1,20 @@ +import { Contract } from "../common/di/contract"; + +/** + * Describes the host operating system the CLI runs on. + */ +@Contract({ name: "hostInfo" }) +export abstract class HostInfo { + abstract isWindows: boolean; + abstract isWindows64: boolean; + abstract isWindows32: boolean; + abstract isDarwin: boolean; + abstract isLinux: boolean; + abstract isLinux64: boolean; + + abstract dotNetVersion(): Promise; + + abstract isDotNet40Installed(message: string): Promise; + + abstract getMacOSVersion(): Promise; +} diff --git a/lib/contracts/http-client.ts b/lib/contracts/http-client.ts new file mode 100644 index 0000000000..9176dcf7eb --- /dev/null +++ b/lib/contracts/http-client.ts @@ -0,0 +1,14 @@ +import { Contract } from "../common/di/contract"; +import type { IProxySettings, Server } from "../common/declarations"; + +/** + * Performs HTTP requests, honouring the CLI's proxy settings. + */ +@Contract({ name: "httpClient" }) +export abstract class HttpClient { + abstract httpRequest(url: string): Promise; + abstract httpRequest( + options: any, + proxySettings?: IProxySettings, + ): Promise; +} diff --git a/lib/contracts/index.ts b/lib/contracts/index.ts new file mode 100644 index 0000000000..c0c8e74d0e --- /dev/null +++ b/lib/contracts/index.ts @@ -0,0 +1,76 @@ +// The `nativescript/contracts` entry point (resolved via contracts/package.json +// — deliberately no `exports` map, so existing deep requires keep working). +// +// This module must stay side-effect-free: an extension's duplicated CLI copy +// may load it, and it must never boot a second runtime. In particular nothing +// here may import lib/common/yok (whose import creates global.$injector). + +export { + Contract, + getContractName, + CONTRACT_NAME, +} from "../common/di/contract"; +export type { IContractOptions } from "../common/di/contract"; +export { + InjectionToken, + getInjectionTokenName, + INJECTION_TOKEN_NAME, +} from "../common/di/injection-token"; +export { inject, runInInjectionContext } from "../common/di/inject"; +export { forwardRef, resolveForwardRef } from "../common/di/forward-ref"; +export { Injector } from "../common/di/injector"; +export type { InjectOptions } from "../common/di/injector"; +export { provide, provideLazy } from "../common/di/providers"; +export type { + Provider, + ProviderToken, + Type, + AbstractType, +} from "../common/di/providers"; + +export { ChildProcess } from "./child-process"; +export { DevicesService } from "./devices-service"; +export { DoctorService } from "./doctor-service"; +export { Errors } from "./errors"; +export { FileSystem } from "./file-system"; +export { HostInfo } from "./host-info"; +export { HttpClient } from "./http-client"; +export { Logger } from "./logger"; +export { PackageManager } from "./package-manager"; +export { ProjectData } from "./project-data"; +export { ProjectDataService } from "./project-data-service"; +export { ProjectNameService } from "./project-name-service"; +export { Prompter } from "./prompter"; +export { TempService } from "./temp-service"; +export { ViteHmrPortService } from "./vite-hmr-port-service"; + +export { PBXPROJ_DOM_XCODE } from "./pbxproj-dom-xcode"; +export { XCODE } from "./xcode"; + +export { + defineCommand, + isCommandDefinition, + booleanOption, + stringOption, + numberOption, + arrayOption, +} from "../common/define-command"; +export type { + CommandDefinition, + DefinedCommand, + CommandContext, + CommandOptionSpec, + DefaultedCommandOptionSpec, + CommandOptionsSchema, + CommandOptionSpecInit, + CommandOptionType, + CommandOptionValues, +} from "../common/define-command"; +export { defineHook, isHookDefinition } from "../common/define-hook"; +export type { + HookContext, + HookDefinition, + HookDefinitionInput, + HookHandler, + HookMiddleware, +} from "../common/define-hook"; diff --git a/lib/contracts/logger.ts b/lib/contracts/logger.ts new file mode 100644 index 0000000000..db2bf4cc02 --- /dev/null +++ b/lib/contracts/logger.ts @@ -0,0 +1,33 @@ +import { Contract } from "../common/di/contract"; + +/** + * Writes the CLI's output, filtered by the configured log level. + */ +@Contract({ name: "logger" }) +export abstract class Logger { + abstract initialize(opts?: ILoggerOptions): void; + + abstract initializeCliLogger(opts?: ILoggerOptions): void; + + abstract getLevel(): string; + + abstract fatal(formatStr?: any, ...args: any[]): void; + + abstract error(formatStr?: any, ...args: any[]): void; + + abstract warn(formatStr?: any, ...args: any[]): void; + + abstract info(formatStr?: any, ...args: any[]): void; + + abstract debug(formatStr?: any, ...args: any[]): void; + + abstract trace(formatStr?: any, ...args: any[]): void; + + abstract printMarkdown(...args: any[]): void; + + abstract prepare(item: any): string; + + abstract isVerbose(): boolean; + + abstract clearScreen(): void; +} diff --git a/lib/contracts/package-manager.ts b/lib/contracts/package-manager.ts new file mode 100644 index 0000000000..cae1a8eed2 --- /dev/null +++ b/lib/contracts/package-manager.ts @@ -0,0 +1,119 @@ +import { Contract } from "../common/di/contract"; +import type { IDictionary } from "../common/declarations"; +import type { + INodePackageManagerInstallOptions, + INpmInstallResultInfo, + INpmPackageNameParts, + INpmsResult, +} from "../declarations"; + +/** + * Dispatches package operations to the package manager selected for the current + * process (npm, yarn, yarn2, pnpm or bun). + */ +@Contract({ name: "packageManager" }) +export abstract class PackageManager { + /** + * Installs dependency + * @param {string} packageName The name of the dependency - can be a path, a url or a string. + * @param {string} pathToSave The destination of the installation. + * @param {INodePackageManagerInstallOptions} config Additional options that can be passed to manipulate installation. + * @return {Promise} Information about installed package. + */ + abstract install( + packageName: string, + pathToSave: string, + config: INodePackageManagerInstallOptions, + ): Promise; + + /** + * Uninstalls a dependency + * @param {string} packageName The name of the dependency. + * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {string} path The destination of the uninstallation. + * @return {Promise} The output of the uninstallation. + */ + abstract uninstall( + packageName: string, + config?: IDictionary, + path?: string, + ): Promise; + + /** + * Provides information about a given package. + * @param {string} packageName The name of the package. + * @param {IDictionary} config Additional options that can be passed to manipulate view. + * @return {Promise} Object, containing information about the package. + */ + abstract view(packageName: string, config: Object): Promise; + + /** + * Checks if the specified string is name of a packaged published in the NPM registry. + * @param {string} packageName The string to be checked. + * @return {Promise} True if the specified string is a registered package name, false otherwise. + */ + abstract isRegistered(packageName: string): Promise; + + /** + * Separates the package name and version from a specified fullPackageName. + * @param {string} fullPackageName The full name of the package like nativescript@10.0.0. + * @return {INpmPackageNameParts} An object containing the separated package name and version. + */ + abstract getPackageNameParts( + fullPackageName: string, + ): Promise; + + /** + * Returns the full name of an npm package based on the provided name and version. + * @param {INpmPackageNameParts} packageNameParts An object containing the package name and version. + * @return {string} The full name of the package like nativescript@10.0.0. + */ + abstract getPackageFullName( + packageNameParts: INpmPackageNameParts, + ): Promise; + + /** + * Searches for a package. + * @param {string[]} filter Keywords with which to perform the search. + * @param {IDictionary} config Additional options that can be passed to manipulate search. + * @return {Promise} The output of the uninstallation. + */ + abstract search( + filter: string[], + config: IDictionary, + ): Promise; + + /** + * Searches for npm packages in npms by keyword. + * @param {string} keyword The keyword based on which the search action will be executed. + * @returns {INpmsResult} The information about found npm packages. + */ + abstract searchNpms(keyword: string): Promise; + + /** + * Gets information for a specified package from registry.npmjs.org. + * @param {string} packageName The name of the package. + * @returns {any} The full data from registry.npmjs.org for this package. + */ + abstract getRegistryPackageData(packageName: string): Promise; + + /** + * Gets the path to npm cache directory. + * @returns {string} The full path to npm cache directory + */ + abstract getCachePath(): Promise; + + /** + * Gets the name of the package manager used for the current process. + * It can be read from the user settings or by passing -- option. + */ + abstract getPackageManagerName(): Promise; + + /** + * Gets the version corresponding to the tag for the package + * @param {string} packageName The name of the package. + * @param {string} tag The tag which we need the version of. + * @returns {string} The version corresponding to the tag + */ + abstract getTagVersion(packageName: string, tag: string): Promise; +} diff --git a/lib/contracts/pbxproj-dom-xcode.ts b/lib/contracts/pbxproj-dom-xcode.ts new file mode 100644 index 0000000000..9c6f8957d2 --- /dev/null +++ b/lib/contracts/pbxproj-dom-xcode.ts @@ -0,0 +1,12 @@ +import { InjectionToken } from "../common/di/injection-token"; +// Type-only: this entry point must stay side-effect-free, and the token is an +// alias over the registration in lib/node/pbxproj-dom-xcode.ts, not a second +// loader. +import type * as pbxprojDomXcode from "pbxproj-dom/xcode"; + +/** + * DOM-style reader/writer for Xcode project files. + */ +export const PBXPROJ_DOM_XCODE = new InjectionToken( + "pbxprojDomXcode", +); diff --git a/lib/contracts/project-data-service.ts b/lib/contracts/project-data-service.ts new file mode 100644 index 0000000000..cfdbe4964a --- /dev/null +++ b/lib/contracts/project-data-service.ts @@ -0,0 +1,132 @@ +import { Contract } from "../common/di/contract"; +import type { SupportedPlatform } from "../constants"; +import type { IProjectDir } from "../common/declarations"; +import type { IBasePluginData } from "../definitions/plugins"; +import type { + IAssetGroup, + IAssetsStructure, + IProjectData, +} from "../definitions/project"; + +/** + * Reads and mutates the metadata of a NativeScript project - the `nativescript` + * key in package.json, `nativescript.config` and the App_Resources assets. + */ +@Contract({ name: "projectDataService" }) +export abstract class ProjectDataService { + /** + * Returns a value from `nativescript` key in project's package.json. + * @param {string} projectDir The project directory - the place where the root package.json is located. + * @param {string} propertyName The name of the property to be checked in `nativescript` key. + * @returns {any} The value of the property. + */ + abstract getNSValue(projectDir: string, propertyName: string): any; + + /** + * Sets a value in the `nativescript` key in a project's package.json. + * @param {string} projectDir The project directory - the place where the root package.json is located. + * @param {string} key Key to be added to `nativescript` key in project's package.json. + * @param {any} value Value of the key to be added to `nativescript` key in project's package.json. + * @returns {void} + */ + abstract setNSValue(projectDir: string, key: string, value: any): void; + + /** + * Removes a property from `nativescript` key in project's package.json. + * @param {string} projectDir The project directory - the place where the root package.json is located. + * @param {string} propertyName The name of the property to be removed from `nativescript` key. + * @returns {void} + */ + abstract removeNSProperty(projectDir: string, propertyName: string): void; + + /** + * Removes a property from `nativescript.config`. + * @param {string} projectDir The project directory - the place where the `nativescript.config` is located. + * @param {string} propertyName The name of the property to be removed. + * @returns {void} + */ + abstract removeNSConfigProperty( + projectDir: string, + propertyName: string, + ): void; + + /** + * Removes dependency from package.json + * @param {string} projectDir The project directory - the place where the root package.json is located. + * @param {string} dependencyName Name of the dependency that has to be removed. + * @returns {void} + */ + abstract removeDependency(projectDir: string, dependencyName: string): void; + + abstract getProjectData(projectDir?: string): IProjectData; + + /** + * Builds the project data from an in-memory package.json instead of reading + * it from disk. Used when the package.json content is not (yet) written out. + * @param {string} packageJsonContent The content of the project's package.json. + * @param {string} projectDir The project directory. Defaults to the current project directory. + * @returns {IProjectData} The project data described by the passed content. + */ + abstract getProjectDataFromContent( + packageJsonContent: string, + projectDir?: string, + ): IProjectData; + + /** + * Serializes the default `nativescript.config` content, optionally merged + * with the passed overrides. + * @param {Object} data Values to merge on top of the defaults. + * @returns {string} The configuration as a JSON string. + */ + abstract getNsConfigDefaultContent(data?: Object): string; + + /** + * Gives information about the whole assets structure for both iOS and Android. + * For each of the platforms, the returned object will contain icons, splashBackgrounds, splashCenterImages and splashImages (only for iOS). + * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. + * @returns {Promise} An object describing the current asset structure. + */ + abstract getAssetsStructure(opts: IProjectDir): Promise; + + /** + * Gives information about the whole assets structure for iOS. + * The returned object will contain icons, splashBackgrounds, splashCenterImages and splashImages. + * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. + * @returns {Promise} An object describing the current asset structure for iOS. + */ + abstract getIOSAssetsStructure(opts: IProjectDir): Promise; + + /** + * Gives information about the whole assets structure for Android. + * The returned object will contain icons, splashBackgrounds and splashCenterImages. + * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. + * @returns {Promise} An object describing the current asset structure for Android. + */ + abstract getAndroidAssetsStructure(opts: IProjectDir): Promise; + + /** + * Returns array with paths to all `.js` or `.ts` files in application's app directory. + * @param {string} projectDir Path to application. + * @returns {string[]} Array of paths to `.js` or `.ts` files. + */ + abstract getAppExecutableFiles(projectDir: string): string[]; + + /** + * Returns package details for runtime, respecting the nativescript key for legacy projects + * @param {string} projectDir Path to application. + * @param {string} platform Platform key + */ + abstract getRuntimePackage( + projectDir: string, + platform: SupportedPlatform, + ): IBasePluginData; + + /** + * Returns a value from `nativescript` key in project's package.json. + * @param {string} jsonData The project directory - the place where the root package.json is located. + * @param {string} propertyName The name of the property to be checked in `nativescript` key. + * @returns {any} The value of the property. + * @deprecated no longer used - will be removed in 8.0. + */ + abstract getNSValueFromContent(jsonData: Object, propertyName: string): any; +} diff --git a/lib/contracts/project-data.ts b/lib/contracts/project-data.ts new file mode 100644 index 0000000000..976d8a0ec6 --- /dev/null +++ b/lib/contracts/project-data.ts @@ -0,0 +1,94 @@ +import { Contract } from "../common/di/contract"; +import type { IStringDictionary } from "../common/declarations"; +import type { BundlerType, INsConfig } from "../definitions/project"; + +/** + * Describes a NativeScript project — its layout on disk, its resolved + * configuration and the dependencies declared in its package.json. + */ +@Contract({ name: "projectData" }) +export abstract class ProjectData { + /** Root directory of the project. */ + abstract projectDir: string; + + /** Name of the application. */ + abstract projectName: string; + + abstract platformsDir: string; + abstract projectFilePath: string; + + /** + * @deprecated Use `projectIdentifiers[platform]` instead. + */ + abstract projectId: string; + + abstract projectIdentifiers?: Mobile.IProjectIdentifier; + abstract dependencies: any; + abstract ignoredDependencies?: string[]; + abstract devDependencies: IStringDictionary; + abstract appDirectoryPath: string; + abstract appResourcesDirectoryPath: string; + abstract projectType: string; + abstract packageJsonData: any; + abstract nsConfig: INsConfig; + abstract androidManifestPath: string; + abstract appGradlePath: string; + abstract gradleFilesDirectoryPath: string; + abstract infoPlistPath: string; + abstract buildXcconfigPath: string; + abstract podfilePath: string; + abstract initialized?: boolean; + + /** + * Defines if the project is a code sharing one. + * Value is true when project has nativescript.config and it has `shared: true` in it. + */ + abstract isShared: boolean; + + /** + * Specifies the bundler used to build the application. + * + * - `"webpack"`: Uses Webpack for traditional bundling. + * - `"rspack"`: Uses Rspack for fast bundling. + * - `"vite"`: Uses Vite for fast bundling. + * + * @default "webpack" + */ + abstract bundler: BundlerType; + + /** + * @deprecated Use bundlerConfigPath + * Defines the path to the configuration file passed to webpack process. + * By default this is the webpack.config.js at the root of the application. + * The value can be changed by setting `webpackConfigPath` in nativescript.config. + */ + abstract webpackConfigPath: string; + + /** + * Defines the path to the bundler configuration file passed to the compiler. + * The value can be changed by setting `bundlerConfigPath` in nativescript.config. + */ + abstract bundlerConfigPath: string; + + /** + * Initializes project data with the given project directory. If none supplied defaults to --path option or cwd. + * @param {string} projectDir Project root directory. + * @returns {void} + */ + abstract initializeProjectData(projectDir?: string): void; + + abstract initializeProjectDataFromContent( + packageJsonContent: string, + projectDir?: string, + ): void; + + abstract getAppDirectoryPath(projectDir?: string): string; + + abstract getAppDirectoryRelativePath(): string; + + abstract getAppResourcesDirectoryPath(projectDir?: string): string; + + abstract getAppResourcesRelativeDirectoryPath(): string; + + abstract getBuildRelativeDirectoryPath(): string; +} diff --git a/lib/contracts/project-name-service.ts b/lib/contracts/project-name-service.ts new file mode 100644 index 0000000000..84d4f9f9f9 --- /dev/null +++ b/lib/contracts/project-name-service.ts @@ -0,0 +1,13 @@ +import { Contract } from "../common/di/contract"; + +@Contract({ name: "projectNameService" }) +export abstract class ProjectNameService { + /** + * Ensures the passed project name is valid; prompts for action otherwise. + * @returns The selected name of the project. + */ + abstract ensureValidName( + projectName: string, + validateOptions?: { force: boolean }, + ): Promise; +} diff --git a/lib/contracts/prompter.ts b/lib/contracts/prompter.ts new file mode 100644 index 0000000000..0687f4112c --- /dev/null +++ b/lib/contracts/prompter.ts @@ -0,0 +1,42 @@ +import { Contract } from "../common/di/contract"; +import type { + IAllowEmpty, + IPrompterOptions, + IPrompterQuestion, +} from "../common/declarations"; + +/** + * Asks the user questions on the terminal. + */ +@Contract({ name: "prompter" }) +export abstract class Prompter { + /** Closes the Ctrl+C reader the prompter installs on stdin. */ + abstract dispose(): void; + + abstract get(schemas: IPrompterQuestion[]): Promise; + + abstract getPassword(prompt: string, options?: IAllowEmpty): Promise; + + abstract getString( + prompt: string, + options?: IPrompterOptions, + ): Promise; + + abstract promptForChoice( + promptMessage: string, + choices: + string[] | { title: string; description?: string; value?: string }[], + multiple?: boolean, + options?: any, + ): Promise; + + abstract promptForDetailedChoice( + promptMessage: string, + choices: { key: string; description: string }[], + ): Promise; + + abstract confirm( + prompt: string, + defaultAction?: () => boolean, + ): Promise; +} diff --git a/lib/contracts/temp-service.ts b/lib/contracts/temp-service.ts new file mode 100644 index 0000000000..4b94c22a4e --- /dev/null +++ b/lib/contracts/temp-service.ts @@ -0,0 +1,12 @@ +import { Contract } from "../common/di/contract"; +import type { AffixOptions } from "../definitions/temp-service"; + +/** + * Creates temporary files and directories that are cleaned up on exit. + */ +@Contract({ name: "tempService" }) +export abstract class TempService { + abstract mkdirSync(affixes: string | AffixOptions): Promise; + + abstract path(options: string | AffixOptions): Promise; +} diff --git a/lib/contracts/vite-hmr-port-service.ts b/lib/contracts/vite-hmr-port-service.ts new file mode 100644 index 0000000000..a23bbca69f --- /dev/null +++ b/lib/contracts/vite-hmr-port-service.ts @@ -0,0 +1,20 @@ +import { Contract } from "../common/di/contract"; + +/** + * Chooses the local port the Vite HMR dev server binds for a platform. + */ +@Contract({ name: "viteHmrPortService" }) +export abstract class ViteHmrPortService { + /** + * Resolves the port the Vite dev server for `platform` listens on: the + * first free port at or above `NS_HMR_PORT` (default 5173) that no other + * platform in this process holds. Resolved once per platform and stable + * for the life of the process, so the build watcher (which bakes the port + * into `bundle.mjs`), the dev server and the Android `adb reverse` tunnel + * all agree on it. + * + * With `NS_HMR_STRICT_PORT` set, a busy preferred port fails instead of + * moving to the next one. + */ + abstract getPort(platform: string): Promise; +} diff --git a/lib/contracts/xcode.ts b/lib/contracts/xcode.ts new file mode 100644 index 0000000000..ce7d8b6399 --- /dev/null +++ b/lib/contracts/xcode.ts @@ -0,0 +1,9 @@ +import { InjectionToken } from "../common/di/injection-token"; +// Type-only: this entry point must stay side-effect-free, and the token is an +// alias over the registration in lib/node/xcode.ts, not a second loader. +import type * as xcode from "nativescript-dev-xcode"; + +/** + * Reads and edits `.pbxproj` files. + */ +export const XCODE = new InjectionToken("xcode"); diff --git a/lib/controllers/deploy-controller.ts b/lib/controllers/deploy-controller.ts index beea13ef08..68607f2a54 100644 --- a/lib/controllers/deploy-controller.ts +++ b/lib/controllers/deploy-controller.ts @@ -1,11 +1,11 @@ import * as _ from "lodash"; import { injector } from "../common/yok"; -export class DeployController { +export class DeployController implements IDeployController { constructor( private $deviceInstallAppService: IDeviceInstallAppService, private $devicesService: Mobile.IDevicesService, - private $prepareController: IPrepareController + private $prepareController: IPrepareController, ) {} public async deploy(data: IDeployData): Promise { @@ -14,7 +14,7 @@ export class DeployController { const executeAction = async (device: Mobile.IDevice) => { const deviceDescriptor = _.find( deviceDescriptors, - (dd) => dd.identifier === device.deviceInfo.identifier + (dd) => dd.identifier === device.deviceInfo.identifier, ); const prepareData = { ...deviceDescriptor.buildData, @@ -27,7 +27,7 @@ export class DeployController { await this.$deviceInstallAppService.installOnDevice( device, { ...deviceDescriptor.buildData, buildForDevice: !device.isEmulator }, - packageFilePath + packageFilePath, ); }; @@ -37,8 +37,8 @@ export class DeployController { _.some( deviceDescriptors, (deviceDescriptor) => - deviceDescriptor.identifier === device.deviceInfo.identifier - ) + deviceDescriptor.identifier === device.deviceInfo.identifier, + ), ); } } diff --git a/lib/controllers/migrate-controller.ts b/lib/controllers/migrate-controller.ts index 3db61d6cd4..a758332ef3 100644 --- a/lib/controllers/migrate-controller.ts +++ b/lib/controllers/migrate-controller.ts @@ -122,7 +122,7 @@ export class MigrateController { packageName: "@nativescript/core", minVersion: "6.5.0", - desiredVersion: "~9.0.0", + desiredVersion: "~9.1.0", shouldAddIfMissing: true, }, { @@ -132,7 +132,7 @@ export class MigrateController { packageName: "@nativescript/types", minVersion: "7.0.0", - desiredVersion: "~9.0.0", + desiredVersion: "~9.1.0", isDev: true, }, { @@ -191,7 +191,7 @@ export class MigrateController { packageName: "@nativescript/angular", minVersion: "10.0.0", - desiredVersion: "^20.0.0", + desiredVersion: "~22.0.0", async shouldMigrateAction( dependency: IMigrationDependency, projectData: IProjectData, @@ -242,7 +242,7 @@ export class MigrateController { packageName: "@nativescript/unit-test-runner", minVersion: "1.0.0", - desiredVersion: "~3.0.0", + desiredVersion: "~4.0.1", async shouldMigrateAction( dependency: IMigrationDependency, projectData: IProjectData, @@ -263,7 +263,7 @@ export class MigrateController packageName: "typescript", isDev: true, minVersion: "3.7.0", - desiredVersion: "~5.8.0", + desiredVersion: "~6.0.0", }, { packageName: "node-sass", @@ -296,13 +296,13 @@ export class MigrateController { packageName: "@nativescript/ios", minVersion: "6.5.3", - desiredVersion: "~9.0.0", + desiredVersion: "~9.1.0", isDev: true, }, { packageName: "@nativescript/android", minVersion: "7.0.0", - desiredVersion: "~9.0.0", + desiredVersion: "~9.1.0", isDev: true, }, ]; @@ -722,7 +722,7 @@ export class MigrateController private async cleanUpProject(projectData: IProjectData): Promise { await this.$projectCleanupService.clean([ constants.HOOKS_DIR_NAME, - constants.PLATFORMS_DIR_NAME, + projectData.getBuildRelativeDirectoryPath(), constants.NODE_MODULES_FOLDER_NAME, constants.PACKAGE_LOCK_JSON_FILE_NAME, ]); @@ -1246,6 +1246,8 @@ export class MigrateController ...new Set([...(configContents.compilerOptions.lib || []), "ESNext"]), ]; + this.migrateTSConfigForTypeScript6(configContents.compilerOptions); + if (isAngular) { // make sure polyfills.ts is in files if (configContents.files) { @@ -1267,6 +1269,43 @@ export class MigrateController } } + // TypeScript 6 errors on 'baseUrl' and 'downlevelIteration', rejects non-relative + // 'paths' targets once 'baseUrl' is gone, drops the implicit node_modules/@types entry + // whenever 'typeRoots' is set, and flips the 'strict' default from false to true. + private migrateTSConfigForTypeScript6(compilerOptions: any): void { + const baseUrl = compilerOptions.baseUrl ?? "."; + delete compilerOptions.baseUrl; + delete compilerOptions.downlevelIteration; + + // rebase against the dropped baseUrl, since targets now resolve from the tsconfig + const toRelative = (target: string) => { + if (path.posix.isAbsolute(target)) { + return target; + } + const rebased = path.posix.join(baseUrl, target); + return rebased.startsWith(".") ? rebased : `./${rebased}`; + }; + + if (compilerOptions.paths) { + compilerOptions.paths = _.mapValues( + compilerOptions.paths, + (targets: string[]) => targets.map(toRelative), + ); + } + + if (compilerOptions.typeRoots) { + compilerOptions.typeRoots = [ + ...new Set([ + ...compilerOptions.typeRoots.map(toRelative), + "./node_modules/@types", + ]), + ]; + } + + compilerOptions.strict = compilerOptions.strict ?? false; + compilerOptions.skipLibCheck = compilerOptions.skipLibCheck ?? true; + } + private async checkOrCreatePolyfillsTS( projectData: IProjectData, ): Promise { @@ -1311,7 +1350,7 @@ export class MigrateController private async migrateNativeScriptAngular(): Promise { const minVersion = "10.0.0"; - const desiredVersion = "~20.2.0"; + const desiredVersion = "~22.0.0"; const dependencies: IMigrationDependency[] = [ { @@ -1371,7 +1410,7 @@ export class MigrateController { packageName: "zone.js", minVersion: "0.11.1", - desiredVersion: "~0.15.0", + desiredVersion: "~0.16.0", shouldAddIfMissing: true, }, diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index b92ea3bbf8..26e480db97 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -18,7 +18,10 @@ import { CONFIG_FILE_NAME_TS, PACKAGE_JSON_FILE_NAME, PLATFORMS_DIR_NAME, + PlatformTypes, PREPARE_READY_EVENT_NAME, + SCOPED_ANDROID_RUNTIME_NAME, + SCOPED_IOS_RUNTIME_NAME, SupportedPlatform, TrackActionNames, } from "../constants"; @@ -31,11 +34,13 @@ import { } from "../definitions/platform"; import { IPluginsService } from "../definitions/plugins"; import { + INsConfig, IProjectConfigService, IProjectData, IProjectDataService, IProjectService, } from "../definitions/project"; +import { resolvePackageJSONPath } from "@rigor789/resolve-package-path"; interface IPlatformWatcherData { hasWebpackCompilerProcess: boolean; @@ -47,7 +52,10 @@ interface IPlatformWatcherData { }; } -export class PrepareController extends EventEmitter { +export class PrepareController + extends EventEmitter + implements IPrepareController +{ private watchersData: IDictionary> = {}; private isInitialPrepareReady = false; private persistedData: IFilesChangeEventData[] = []; @@ -447,30 +455,83 @@ export class PrepareController extends EventEmitter { this.$logger.info( "Updating runtime package.json with configuration values...", ); - const nsConfig = this.$projectConfigService.readConfig( - projectData.projectDir, + + // Tolerate a missing/unreadable config — prior to the destructure this + // was a plain object spread, where `undefined` is a legal no-op. + const { + hooks, + ignoredNativeDependencies, + webpackPackageName, + webpackConfigPath, + appResourcesPath, + buildPath, + appPath, + ...nsConfig + } = + this.$projectConfigService.readConfig(projectData.projectDir) ?? + ({} as INsConfig); + + const platform = platformData.platformNameLowerCase; + let installedRuntimePackageJSON; + let runtimePackageName: string; + if (platform === PlatformTypes.ios) { + runtimePackageName = + projectData.nsConfig?.ios?.runtimePackageName || + SCOPED_IOS_RUNTIME_NAME; + } else if (platform === PlatformTypes.android) { + runtimePackageName = + projectData.nsConfig?.android?.runtimePackageName || + SCOPED_ANDROID_RUNTIME_NAME; + } + // try reading from installed runtime first before reading from the npm registry... + const installedRuntimePackageJSONPath = resolvePackageJSONPath( + runtimePackageName, + { + paths: [projectData.projectDir], + }, ); + + if (installedRuntimePackageJSONPath) { + installedRuntimePackageJSON = this.$fs.readJson( + installedRuntimePackageJSONPath, + ); + } const packageData: any = { ..._.pick(projectData.packageJsonData, ["name"]), ...nsConfig, main: "bundle", + ...(installedRuntimePackageJSON ? {} : {}), }; - - if ( - platformData.platformNameLowerCase === "ios" && - packageData.ios && - packageData.ios.discardUncaughtJsExceptions - ) { - packageData.discardUncaughtJsExceptions = - packageData.ios.discardUncaughtJsExceptions; + if (platform === PlatformTypes.ios) { + if (installedRuntimePackageJSON) { + packageData.ios = packageData.ios || {}; + packageData.ios.runtime = { + version: installedRuntimePackageJSON.version, + }; + } + if (packageData.ios && packageData.ios.discardUncaughtJsExceptions) { + packageData.discardUncaughtJsExceptions = + packageData.ios.discardUncaughtJsExceptions; + } + delete packageData.android; } - if ( - platformData.platformNameLowerCase === "android" && - packageData.android && - packageData.android.discardUncaughtJsExceptions - ) { - packageData.discardUncaughtJsExceptions = - packageData.android.discardUncaughtJsExceptions; + if (platform === PlatformTypes.android) { + if (installedRuntimePackageJSON) { + packageData.android = packageData.android || {}; + packageData.android.runtime = { + version: installedRuntimePackageJSON.version, + version_info: installedRuntimePackageJSON.version_info, + gradle: installedRuntimePackageJSON.gradle, + }; + } + if ( + packageData.android && + packageData.android.discardUncaughtJsExceptions + ) { + packageData.discardUncaughtJsExceptions = + packageData.android.discardUncaughtJsExceptions; + } + delete packageData.ios; } let packagePath: string; if ( diff --git a/lib/controllers/run-controller.ts b/lib/controllers/run-controller.ts index b1860c5ead..925ffd3fc7 100644 --- a/lib/controllers/run-controller.ts +++ b/lib/controllers/run-controller.ts @@ -7,6 +7,7 @@ import { USER_INTERACTION_NEEDED_EVENT_NAME, } from "../constants"; import { cache, performanceLog } from "../common/decorators"; +import { isTruthyEnvFlag } from "../common/helpers"; import { EventEmitter } from "events"; import * as util from "util"; import * as _ from "lodash"; @@ -22,10 +23,20 @@ import { IDictionary, } from "../common/declarations"; import { IInjector } from "../common/definitions/yok"; +import { ViteHmrPortService } from "../contracts/vite-hmr-port-service"; import { injector } from "../common/yok"; export class RunController extends EventEmitter implements IRunController { private prepareReadyEventHandler: any = null; + private _syncInProgress = false; + private _pendingSyncs: Map< + string, + { + data: IFilesChangeEventData; + projectData: IProjectData; + liveSyncInfo: ILiveSyncInfo; + } + > = new Map(); constructor( protected $analyticsService: IAnalyticsService, @@ -48,6 +59,8 @@ export class RunController extends EventEmitter implements IRunController { private $prepareNativePlatformService: IPrepareNativePlatformService, private $projectChangesService: IProjectChangesService, protected $projectDataService: IProjectDataService, + private $staticConfig: Config.IStaticConfig, + private $viteHmrPortService: ViteHmrPortService, ) { super(); } @@ -97,16 +110,11 @@ export class RunController extends EventEmitter implements IRunController { projectData, prepareData, ); - if (changesInfo.hasChanges) { - await this.syncChangedDataOnDevices( - data, - projectData, - liveSyncInfo, - ); + if (!changesInfo.hasChanges) { + return; } - } else { - await this.syncChangedDataOnDevices(data, projectData, liveSyncInfo); } + this.scheduleSyncOnDevices(data, projectData, liveSyncInfo); }; this.prepareReadyEventHandler = handler.bind(this); @@ -494,6 +502,20 @@ export class RunController extends EventEmitter implements IRunController { }, ); + // For Android + Vite HMR, own the `adb reverse` ourselves — + // with our SDK-resolved adb, scoped to this exact serial, and + // only after the device is up — then hand the bundler the + // result via env vars. This MUST run before `prepare` (which + // spawns the Vite bundler that inherits `process.env`) so the + // bundler trusts the tunnel instead of racing us to spawn its + // own adb during config-load. See packages/vite hardening. + await this.setupAndroidViteHmrReverse( + device, + projectData, + liveSyncInfo, + "pre-build", + ); + const prepareResultData = await this.$prepareController.prepare(prepareData); @@ -568,6 +590,17 @@ export class RunController extends EventEmitter implements IRunController { liveSyncDeviceData: deviceDescriptor, }); + // Re-establish the adb reverse on the CURRENT transport right + // before launch — the transport can change during build/install + // and drop the mapping set in `pre-build`, which would leave the + // app unable to reach the Vite dev server at 127.0.0.1. + await this.setupAndroidViteHmrReverse( + device, + projectData, + liveSyncInfo, + "pre-launch", + ); + await this.refreshApplication( projectData, liveSyncResultInfo, @@ -622,6 +655,150 @@ export class RunController extends EventEmitter implements IRunController { ); } + /** + * Set up `adb reverse tcp: tcp:` for an Android device + * when the project bundles with Vite in HMR/watch mode, then export + * the result to the bundler subprocess via environment variables. + * + * The Vite dev-host helper prefers an ADB tunnel (device-side + * `127.0.0.1:` → host) over the emulator's flaky slirp NAT + * (`10.0.2.2`). Historically the bundler tried to wire that tunnel + * itself at config-load time, racing this CLI's device discovery + * over the single global adb daemon and intermittently freezing the + * run at "Searching for devices…". The CLI is the right owner: it + * knows the exact target serial and when the device is ready, and it + * already drives a single, version-matched adb. We do the reverse + * here and signal the bundler with `NS_ADB_REVERSE_READY=1` so it + * never spawns adb on its own. + * + * Best-effort: any failure is logged at trace level and swallowed. + * The bundler then falls back to its own (now hardened) adb path, or + * ultimately to `10.0.2.2`, so a reverse hiccup never fails the run. + */ + private async setupAndroidViteHmrReverse( + device: Mobile.IDevice, + projectData: IProjectData, + liveSyncInfo: ILiveSyncInfo, + phase: "pre-build" | "pre-launch", + ): Promise { + try { + if (!this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { + return; + } + if (projectData.bundler !== "vite") { + return; + } + // HMR over the tunnel only matters for a live watch session. + if (liveSyncInfo.skipWatcher || !liveSyncInfo.useHotModuleReload) { + return; + } + // Respect the user's explicit opt-out — they want the + // `10.0.2.2` / LAN path, so don't create a tunnel or claim one + // exists. + if (isTruthyEnvFlag(process.env.NS_HMR_NO_ADB_REVERSE)) { + return; + } + // `NS_HMR_PREFER_LAN_HOST` means the dev wants LAN routing + // (physical device over Wi-Fi); the dev-host resolver suppresses + // the adb-reverse path for it, so don't bother wiring one. + if (isTruthyEnvFlag(process.env.NS_HMR_PREFER_LAN_HOST)) { + return; + } + + const serial = device.deviceInfo.identifier; + const port = await this.$viteHmrPortService.getPort( + device.deviceInfo.platform, + ); + + if (phase === "pre-build") { + // Decide the origin baked into bundle.mjs. Hand the bundler our + // exact adb (so any self-managed fallback can't version-mismatch + // the daemon) and, if the tunnel comes up, tell it to emit + // `127.0.0.1` and skip adb entirely. + process.env.NS_ADB_PATH = await this.$staticConfig.getAdbFilePath(); + process.env.NS_DEVICE_SERIAL = serial; + + const ok = await this.ensureAndroidReverse(device, serial, port); + if (ok) { + process.env.NS_ADB_REVERSE_READY = "1"; + this.$logger.info( + `Set up adb reverse tcp:${port} tcp:${port} for ${serial} (Vite HMR routes device-side 127.0.0.1:${port} through ADB).`, + ); + } else { + this.$logger.warn( + `Could not confirm 'adb reverse tcp:${port}' on ${serial} (device adbd slow/unresponsive). Vite HMR will fall back to 10.0.2.2. If this persists, cold-boot/wipe the emulator, or set NS_HMR_NO_ADB_REVERSE=1.`, + ); + } + return; + } + + // phase === "pre-launch": re-establish the mapping right before the + // app boots. `adb reverse` mappings are bound to the device's adb + // transport, and that transport can change during the (long) build + // + install (fresh emulators reconnect as they settle), silently + // dropping the early mapping. We only bother when we actually told + // the bundle to use `127.0.0.1` (READY set during pre-build). + if (!isTruthyEnvFlag(process.env.NS_ADB_REVERSE_READY)) { + return; + } + const ok = await this.ensureAndroidReverse(device, serial, port); + if (!ok) { + this.$logger.warn( + `adb reverse tcp:${port} was not active before launch on ${serial}; the app may fail to reach the Vite dev server at 127.0.0.1:${port}.`, + ); + } + } catch (err) { + this.$logger.trace( + `Setting up adb reverse for Vite HMR (${phase}) failed; leaving it to the bundler fallback. Error: ${err}`, + ); + } + } + + /** + * Apply `adb reverse tcp: tcp:` to the device and confirm + * via `adb reverse --list` that it actually landed, retrying a few + * times. Every device-side call is bounded with a Node `spawn` timeout + * + `SIGKILL` so a wedged/slow adbd (observed blocking 90s+ on some + * fresh-boot / API-36 arm64 emulators) can never hang the run — the + * hung adb child is reaped, not orphaned. Returns whether the mapping + * is confirmed present. + */ + private async ensureAndroidReverse( + device: Mobile.IDevice, + serial: string, + port: number, + ): Promise { + const adb = (device as Mobile.IAndroidDevice).adb; + const ADB_WAIT_MS = 15000; + const ADB_REVERSE_MS = 20000; + const bounded = (timeout: number) => ({ + deviceIdentifier: serial, + treatErrorsAsWarnings: true, + childProcessOptions: { timeout, killSignal: "SIGKILL" }, + }); + + // `wait-for-device` only blocks until the transport is up; bounded so a + // never-ready device can't stall us. + await adb.executeCommand(["wait-for-device"], bounded(ADB_WAIT_MS)); + + for (let attempt = 1; attempt <= 3; attempt++) { + await adb.executeCommand( + ["reverse", `tcp:${port}`, `tcp:${port}`], + bounded(ADB_REVERSE_MS), + ); + // Verify it landed (a SIGKILL'd-on-timeout reverse resolves rather + // than throws, so success of the call isn't proof). + const list = + ( + await adb.executeCommand(["reverse", "--list"], bounded(ADB_WAIT_MS)) + )?.toString?.() ?? ""; + if (list.includes(`tcp:${port}`)) { + return true; + } + } + return false; + } + private async syncChangedDataOnDevices( data: IFilesChangeEventData, projectData: IProjectData, @@ -840,11 +1017,11 @@ export class RunController extends EventEmitter implements IRunController { watchInfo.connectTimeout = null; await watchAction(); } - } catch (err) { + } catch (err: any) { this.$logger.warn( `Unable to apply changes for device: ${ device.deviceInfo.identifier - }. Error is: ${err && err.message}.`, + }. Error is: ${err && err.message}. Will retry on next change.`, ); this.emitCore(RunOnDeviceEvents.runOnDeviceError, { @@ -856,12 +1033,6 @@ export class RunController extends EventEmitter implements IRunController { ], error: err, }); - - await this.stop({ - projectDir: projectData.projectDir, - deviceIdentifiers: [device.deviceInfo.identifier], - stopOptions: { shouldAwaitAllActions: false }, - }); } }; @@ -885,6 +1056,67 @@ export class RunController extends EventEmitter implements IRunController { ); } + private scheduleSyncOnDevices( + data: IFilesChangeEventData, + projectData: IProjectData, + liveSyncInfo: ILiveSyncInfo, + ): void { + if (this._syncInProgress) { + const platform = data.platform; + const existing = this._pendingSyncs.get(platform); + if (existing) { + existing.data = this.mergeFilesChangeEvents(existing.data, data); + } else { + this._pendingSyncs.set(platform, { data, projectData, liveSyncInfo }); + } + return; + } + + this.executeSyncOnDevices(data, projectData, liveSyncInfo); + } + + private async executeSyncOnDevices( + data: IFilesChangeEventData, + projectData: IProjectData, + liveSyncInfo: ILiveSyncInfo, + ): Promise { + this._syncInProgress = true; + try { + await this.syncChangedDataOnDevices(data, projectData, liveSyncInfo); + } catch (err: any) { + this.$logger.trace(`Error during sync on devices: ${err.message || err}`); + } finally { + const nextEntry = this._pendingSyncs.entries().next(); + if (!nextEntry.done) { + const [platform, pending] = nextEntry.value; + this._pendingSyncs.delete(platform); + this.executeSyncOnDevices( + pending.data, + pending.projectData, + pending.liveSyncInfo, + ); + } else { + this._syncInProgress = false; + } + } + } + + private mergeFilesChangeEvents( + a: IFilesChangeEventData, + b: IFilesChangeEventData, + ): IFilesChangeEventData { + return { + files: [...new Set([...a.files, ...b.files])], + staleFiles: [ + ...new Set([...(a.staleFiles || []), ...(b.staleFiles || [])]), + ], + hasOnlyHotUpdateFiles: a.hasOnlyHotUpdateFiles && b.hasOnlyHotUpdateFiles, + hasNativeChanges: a.hasNativeChanges || b.hasNativeChanges, + hmrData: b.hmrData, + platform: b.platform, + }; + } + private async addActionToChain( projectDir: string, action: () => Promise, @@ -892,13 +1124,17 @@ export class RunController extends EventEmitter implements IRunController { const liveSyncInfo = this.$liveSyncProcessDataService.getPersistedData(projectDir); if (liveSyncInfo) { - liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => { - if (!liveSyncInfo.isStopped) { - liveSyncInfo.currentSyncAction = action(); - const res = await liveSyncInfo.currentSyncAction; - return res; - } - }); + liveSyncInfo.actionsChain = liveSyncInfo.actionsChain + .then(async () => { + if (!liveSyncInfo.isStopped) { + liveSyncInfo.currentSyncAction = action(); + const res = await liveSyncInfo.currentSyncAction; + return res; + } + }) + .catch((err: any) => { + this.$logger.warn(`Error in action chain: ${err.message || err}`); + }); const result = await liveSyncInfo.actionsChain; return result; diff --git a/lib/controllers/update-controller.ts b/lib/controllers/update-controller.ts index bcb4a5abf0..52034bc6b2 100644 --- a/lib/controllers/update-controller.ts +++ b/lib/controllers/update-controller.ts @@ -109,7 +109,7 @@ export class UpdateController // clean up project files this.spinner.info("Cleaning up project files before update"); - await this.cleanUpProject(); + await this.cleanUpProject(projectData); this.spinner.succeed("Project files have been cleaned up"); @@ -293,10 +293,10 @@ export class UpdateController } } - private async cleanUpProject(): Promise { + private async cleanUpProject(projectData: IProjectData): Promise { await this.$projectCleanupService.clean([ constants.HOOKS_DIR_NAME, - constants.PLATFORMS_DIR_NAME, + projectData.getBuildRelativeDirectoryPath(), constants.NODE_MODULES_FOLDER_NAME, constants.PACKAGE_LOCK_JSON_FILE_NAME, ]); diff --git a/lib/data/build-data.ts b/lib/data/build-data.ts index f6b2734174..2dc9ba1546 100644 --- a/lib/data/build-data.ts +++ b/lib/data/build-data.ts @@ -51,6 +51,7 @@ export class AndroidBuildData extends BuildData { public keyStoreAliasPassword: string; public keyStorePassword: string; public androidBundle: boolean; + public gradleFlavor: string; public gradlePath: string; public gradleArgs: string; public hostProjectPath: string; @@ -63,6 +64,7 @@ export class AndroidBuildData extends BuildData { this.keyStoreAliasPassword = data.keyStoreAliasPassword; this.keyStorePassword = data.keyStorePassword; this.androidBundle = data.androidBundle || data.aab; + this.gradleFlavor = data.gradleFlavor; this.gradlePath = data.gradlePath; this.gradleArgs = data.gradleArgs; this.hostProjectPath = data.hostProjectPath; diff --git a/lib/data/prepare-data.ts b/lib/data/prepare-data.ts index 2af06adbeb..0df388906f 100644 --- a/lib/data/prepare-data.ts +++ b/lib/data/prepare-data.ts @@ -14,7 +14,7 @@ export class PrepareData extends ControllerDataBase { constructor( public projectDir: string, public platform: string, - data: IOptions + data: IOptions, ) { super(projectDir, platform, data); @@ -45,6 +45,11 @@ export class PrepareData extends ControllerDataBase { } this.hostProjectPath = data.hostProjectPath; + if (data.skipNative) { + this.nativePrepare = { skipNativePrepare: true }; + this.watchNative = false; + } + this.uniqueBundle = !this.watch && data.uniqueBundle ? Date.now() : 0; } } diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index edb18142ed..79f4559d76 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -19,6 +19,8 @@ import { } from "./common/declarations"; import { IExtensionData } from "./common/definitions/extensibility"; import { IApplePortalUserDetail } from "./services/apple-portal/definitions"; +import type { ProjectNameService } from "./contracts/project-name-service"; +import type { PackageManager } from "./contracts/package-manager"; interface INodePackageManager { /** @@ -31,7 +33,7 @@ interface INodePackageManager { install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise; /** @@ -44,7 +46,7 @@ interface INodePackageManager { uninstall( packageName: string, config?: IDictionary, - path?: string + path?: string, ): Promise; /** @@ -84,7 +86,7 @@ interface INodePackageManager { */ search( filter: string[], - config: IDictionary + config: IDictionary, ): Promise; /** @@ -108,21 +110,8 @@ interface INodePackageManager { getCachePath(): Promise; } -interface IPackageManager extends INodePackageManager { - /** - * Gets the name of the package manager used for the current process. - * It can be read from the user settings or by passing -- option. - */ - getPackageManagerName(): Promise; - - /** - * Gets the version corresponding to the tag for the package - * @param {string} packageName The name of the package. - * @param {string} tag The tag which we need the version of. - * @returns {string} The version corresponding to the tag - */ - getTagVersion(packageName: string, tag: string): Promise; -} +/** @deprecated Kept so existing annotations compile; use the {@link PackageManager} contract. */ +interface IPackageManager extends PackageManager {} interface IPerformanceService { // Will process the data based on the command options (--performance flag and user-reporting setting) @@ -130,7 +119,7 @@ interface IPerformanceService { methodInfo: string, startTime: number, endTime: number, - args: any[] + args: any[], ): void; // Will return a reference time in milliseconds @@ -141,39 +130,39 @@ interface IPackageInstallationManager { install( packageName: string, packageDir: string, - options?: INpmInstallOptions + options?: INpmInstallOptions, ): Promise; uninstall( packageName: string, packageDir: string, - options?: IDictionary + options?: IDictionary, ): Promise; getLatestVersion(packageName: string): Promise; getNextVersion(packageName: string): Promise; getLatestCompatibleVersion( packageName: string, - referenceVersion?: string + referenceVersion?: string, ): Promise; getMaxSatisfyingVersion( packageName: string, - versionRange: string + versionRange: string, ): Promise; getLatestCompatibleVersionSafe( packageName: string, - referenceVersion?: string + referenceVersion?: string, ): Promise; getInspectorFromCache( inspectorNpmPackageName: string, - projectDir: string + projectDir: string, ): Promise; clearInspectorCache(): void; getInstalledDependencyVersion( packageName: string, - projectDir?: string + projectDir?: string, ): Promise; getMaxSatisfyingVersionSafe( packageName: string, - versionIdentifier: string + versionIdentifier: string, ): Promise; } @@ -181,8 +170,7 @@ interface IPackageInstallationManager { * Describes options that can be passed to manipulate package installation. */ interface INodePackageManagerInstallOptions - extends INpmInstallConfigurationOptions, - IDictionary { + extends INpmInstallConfigurationOptions, IDictionary { /** * Destination of the installation. * @type {string} @@ -266,7 +254,7 @@ interface INpmPeerDependencyInfo { * @type {string} */ requires: string; - } + }, ]; /** * Dependencies of the dependency. @@ -506,7 +494,8 @@ interface IStaticConfig extends Config.IStaticConfig {} interface IConfiguration extends Config.IConfig { ANDROID_DEBUG_UI: string; USE_POD_SANDBOX: boolean; - GA_TRACKING_ID: string; + GA_MEASUREMENT_ID: string; + GA_API_SECRET: string; } interface IApplicationPackage { @@ -550,8 +539,7 @@ interface INpmInstallConfigurationOptionsBase { ignoreScripts: boolean; //npm flag } -interface INpmInstallConfigurationOptions - extends INpmInstallConfigurationOptionsBase { +interface INpmInstallConfigurationOptions extends INpmInstallConfigurationOptionsBase { disableNpmInstall: boolean; } @@ -584,6 +572,11 @@ interface IEmbedOptions { } interface IAndroidOptions extends IEmbedOptions { + /** + * The product flavor to build, when the app declares any. `--gradleFlavor foo` + * runs `assembleFooDebug` instead of `assembleDebug`. + */ + gradleFlavor: string; gradlePath: string; gradleArgs: string; } @@ -597,7 +590,8 @@ interface ITypingsOptions { } interface IOptions - extends IRelease, + extends + IRelease, IDeviceIdentifier, IJustLaunch, IAvd, @@ -622,7 +616,7 @@ interface IOptions argv: IYargArgv; validateOptions( commandSpecificDashedOptions?: IDictionary, - projectData?: IProjectData + projectData?: IProjectData, ): void; options: IDictionary; shorthands: string[]; @@ -709,6 +703,7 @@ interface IOptions dryRun: boolean; platformOverride: string; + skipNative: boolean; uniqueBundle: boolean; // allow arbitrary options [optionName: string]: any; @@ -719,26 +714,22 @@ interface IEnvOptions { } interface IAndroidBuildOptionsSettings - extends IAndroidReleaseOptions, - IRelease, - Partial {} + extends IAndroidReleaseOptions, IRelease, Partial {} interface IHasAndroidBundle { androidBundle: boolean; } interface IPlatformBuildData - extends IRelease, - IHasUseHotModuleReloadOption, - IBuildConfig, - IEnvOptions {} + extends IRelease, IHasUseHotModuleReloadOption, IBuildConfig, IEnvOptions {} interface IDeviceEmulator extends IHasEmulatorOption, IDeviceIdentifier {} interface IRunPlatformOptions extends IJustLaunch, IDeviceEmulator {} interface IDeployPlatformOptions - extends IAndroidReleaseOptions, + extends + IAndroidReleaseOptions, IRelease, IClean, IDeviceEmulator, @@ -834,7 +825,7 @@ interface IAndroidToolsInfo { */ validateJavacVersion( installedJavaVersion: string, - options?: IAndroidToolsInfoOptions + options?: IAndroidToolsInfoOptions, ): boolean; /** @@ -913,14 +904,14 @@ interface IAppDebugSocketProxyFactory extends NodeJS.EventEmitter { device: Mobile.IiOSDevice, appId: string, projectName: string, - projectDir: string + projectDir: string, ): Promise; ensureWebSocketProxy( device: Mobile.IiOSDevice, appId: string, projectName: string, - projectDir: string + projectDir: string, ): Promise; removeAllProxies(): void; @@ -939,12 +930,12 @@ interface IiOSSocketRequestExecutor { executeAttachRequest( device: Mobile.IiOSDevice, timeout: number, - projectId: string + projectId: string, ): Promise; executeRefreshRequest( device: Mobile.IiOSDevice, timeout: number, - appId: string + appId: string, ): Promise; } @@ -986,18 +977,8 @@ interface IVersionsService { /** * Describes methods for project name. */ -interface IProjectNameService { - /** - * Ensures the passed project name is valid. If the project name is not valid prompts for actions. - * @param {string} projectName project name to be checked. - * @param {IOptions} validateOptions current command options. - * @return {Promise} returns the selected name of the project. - */ - ensureValidName( - projectName: string, - validateOptions?: { force: boolean } - ): Promise; -} +/** @deprecated Kept so existing annotations compile; use the {@link ProjectNameService} contract. */ +interface IProjectNameService extends ProjectNameService {} /** * Describes options that can be passed to xcprojService.verifyXcproj method. @@ -1089,7 +1070,7 @@ interface IBundleValidatorHelper { */ getBundlerDependencyVersion( projectData: IProjectData, - bundlerName?: string + bundlerName?: string, ): string; } @@ -1171,7 +1152,7 @@ interface IAssetsGenerationService { * @returns {Promise} */ generateSplashScreens( - splashesGenerationData: IResourceGenerationData + splashesGenerationData: IResourceGenerationData, ): Promise; } @@ -1183,10 +1164,6 @@ interface IRuntimeGradleVersions { gradleAndroidPluginVersion?: string; } -interface INetworkConnectivityValidator { - validate(): Promise; -} - interface IPlatformValidationService { /** * Ensures the passed platform is a valid one (from the supported ones) @@ -1207,7 +1184,7 @@ interface IPlatformValidationService { provision: true | string, teamId: true | string, projectData: IProjectData, - platform?: string + platform?: string, ): Promise; validatePlatformInstalled(platform: string, projectData: IProjectData): void; @@ -1220,7 +1197,7 @@ interface IPlatformValidationService { */ isPlatformSupportedForOS( platform: string, - projectData: IProjectData + projectData: IProjectData, ): boolean; } @@ -1228,27 +1205,27 @@ interface IPlatformCommandHelper { addPlatforms( platforms: string[], projectData: IProjectData, - frameworkPath?: string + frameworkPath?: string, ): Promise; cleanPlatforms( platforms: string[], projectData: IProjectData, - frameworkPath: string + frameworkPath: string, ): Promise; removePlatforms( platforms: string[], - projectData: IProjectData + projectData: IProjectData, ): Promise; updatePlatforms( platforms: string[], - projectData: IProjectData + projectData: IProjectData, ): Promise; getInstalledPlatforms(projectData: IProjectData): string[]; getAvailablePlatforms(projectData: IProjectData): string[]; getPreparedPlatforms(projectData: IProjectData): string[]; getCurrentPlatformVersion( platform: string, - projectData: IProjectData + projectData: IProjectData, ): string; } diff --git a/lib/definitions/build.d.ts b/lib/definitions/build.d.ts index e64a318c6d..391cb672c1 100644 --- a/lib/definitions/build.d.ts +++ b/lib/definitions/build.d.ts @@ -31,6 +31,7 @@ interface IAndroidBuildData extends IBuildData, IAndroidSigningData, IHasAndroidBundle { + gradleFlavor?: string; gradlePath?: string; gradleArgs?: string; } diff --git a/lib/definitions/deploy.d.ts b/lib/definitions/deploy.d.ts index 07010a2a61..7f6b47e898 100644 --- a/lib/definitions/deploy.d.ts +++ b/lib/definitions/deploy.d.ts @@ -1,3 +1,3 @@ interface IDeployController { - deploy(data: IRunData): Promise; + deploy(data: IDeployData): Promise; } diff --git a/lib/definitions/ios.d.ts b/lib/definitions/ios.d.ts index 81b54c6896..60c3430bbb 100644 --- a/lib/definitions/ios.d.ts +++ b/lib/definitions/ios.d.ts @@ -8,18 +8,18 @@ declare global { setupSigningForDevice( projectRoot: string, projectData: IProjectData, - buildConfig: IOSBuildData + buildConfig: IOSBuildData, ): Promise; setupSigningFromTeam( projectRoot: string, projectData: IProjectData, - teamId: string + teamId: string, ): Promise; setupSigningFromProvision( projectRoot: string, projectData: IProjectData, provision?: string, - mobileProvisionData?: any + mobileProvisionData?: any, ): Promise; } @@ -27,55 +27,99 @@ declare global { buildForSimulator( platformData: IPlatformData, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; buildForDevice( platformData: IPlatformData, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; buildForAppStore( platformData: IPlatformData, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; } - type IosSPMPackage = IosSPMPackageDefinition & { targets?: string[] }; + interface IosSPMPackageBase { + name: string; + /** Swift product names to link from the package. */ + libs: string[]; + /** + * Optional: if the project has additional targets (widgets, watch apps, + * extensions...) list their names here to link the package with them too. + */ + targets?: string[]; + } + + /** A package resolved from a git remote at a version, range, branch or revision. */ + interface IosRemoteSPMPackage extends IosSPMPackageBase { + repositoryURL: string; + version: string; + } + + /** A package resolved from a directory on disk. */ + interface IosLocalSPMPackage extends IosSPMPackageBase { + path: string; + } + + type IosSPMPackage = IosRemoteSPMPackage | IosLocalSPMPackage; + + /** One package linked into one target of the Xcode project. */ + interface IosSPMPackageAssignment { + targetName: string; + package: IosSPMPackage; + } + + interface ISPMPbxprojService { + addPackages( + projectRoot: string, + assignments: IosSPMPackageAssignment[], + ): boolean; + } interface ISPMService { applySPMPackages( platformData: IPlatformData, projectData: IProjectData, - pluginSpmPackages?: IosSPMPackage[] + pluginSpmPackages?: IosSPMPackage[], ); getSPMPackages( projectData: IProjectData, - platform: string + platform: string, ): IosSPMPackage[]; + resolveSPMDependencies( + platformData: IPlatformData, + projectData: IProjectData, + options?: { showProgress?: boolean }, + ): Promise; + ensureSPMDependenciesResolved( + platformData: IPlatformData, + projectData: IProjectData, + ): Promise; } interface IXcodebuildArgsService { getBuildForSimulatorArgs( platformData: IPlatformData, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; getBuildForDeviceArgs( platformData: IPlatformData, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; getXcodeProjectArgs( platformData: IPlatformData, - projectData: IProjectData + projectData: IProjectData, ): string[]; } interface IXcodebuildCommandService { executeCommand( args: string[], - options: IXcodebuildCommandOptions + options: IXcodebuildCommandOptions, ): Promise; } @@ -84,18 +128,24 @@ declare global { cwd: string; stdio?: string; spawnOptions?: any; + /** + * When provided, xcodebuild's output is piped (rather than inherited) and + * forwarded here so the caller can render its own progress UI (e.g. a + * spinner for Swift Package resolution/download activity). + */ + onProgress?: (chunk: { data: string; pipe: string }) => void; } interface IExportOptionsPlistService { createDevelopmentExportOptionsPlist( archivePath: string, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; createDistributionExportOptionsPlist( projectRoot: string, projectData: IProjectData, - buildConfig: IBuildConfig + buildConfig: IBuildConfig, ): Promise; } diff --git a/lib/definitions/nativescript-dev-xcode.d.ts b/lib/definitions/nativescript-dev-xcode.d.ts index 9767be5979..22fcca05f4 100644 --- a/lib/definitions/nativescript-dev-xcode.d.ts +++ b/lib/definitions/nativescript-dev-xcode.d.ts @@ -8,16 +8,27 @@ declare module "nativescript-dev-xcode" { } class project { + hash: any; + filepath: string; constructor(filename: string); parse(callback: () => void): void; parseSync(): void; + generateUuid(): string; + writeSync(options: any): string; addFramework(filepath: string, options?: Options): void; removeFramework(filePath: string, options?: Options): void; + + getProductFile(watchApptarget: target): any; + addToPbxFrameworksBuildPhase(file); + addToPbxCopyfilesBuildPhase(file, comment: string, targetid: string); + pbxFrameworksBuildPhaseObj(targetid: string): any; + pbxBuildFileSection(): {[k: string] : any}; + addPbxGroup( filePathsArray: any[], name: string, @@ -27,17 +38,30 @@ declare module "nativescript-dev-xcode" { removePbxGroup(groupName: string, path: string): void; + addTargetDependency(target: string, dependencyTargets: string[]); + + findTargetKey(name: string); + pbxTargetByName(name: string): target; + pbxNativeTargetSection(): {[key: string]: any}; + addToHeaderSearchPaths(options?: Options): void; removeFromHeaderSearchPaths(options?: Options): void; updateBuildProperty(key: string, value: any): void; pbxXCBuildConfigurationSection(): any; + buildPhaseObject( + buildPhaseType: string, + comment: string, + target: tstring + ) + addTarget( targetName: string, targetType: string, targetPath?: string, - parentTarget?: string + parentTarget?: string, + productTargetType?: string ): target; addBuildPhase( filePathsArray: string[], diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index c8046b2ed4..cd2a931617 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -1,4 +1,4 @@ -import type { SupportedPlatform } from "../constants"; +import type { BuildNames, SupportedPlatform } from "../constants"; import { IAndroidBuildOptionsSettings, IProvision, @@ -18,6 +18,8 @@ import { } from "../common/declarations"; import { SupportedConfigValues } from "../tools/config-manipulation/config-transformer"; import * as constants from "../constants"; +import type { ProjectData } from "../contracts/project-data"; +import type { ProjectDataService } from "../contracts/project-data-service"; interface IProjectName { /** @@ -130,6 +132,10 @@ interface INsConfigIOS extends INsConfigPlaform { * List packages to be included in the iOS build. */ SPMPackages?: Array; + /** + * Custom runtime package name + */ + runtimePackageName?: string; } interface INSConfigVisionOS extends INsConfigIOS {} @@ -168,6 +174,11 @@ interface INsConfigAndroid extends INsConfigPlaform { enableLineBreakpoints?: boolean; enableMultithreadedJavascript?: boolean; + + /** + * Custom runtime package name + */ + runtimePackageName?: string; } interface INsConfigHooks { @@ -180,6 +191,11 @@ interface INsConfig { main?: string; appPath?: string; appResourcesPath?: string; + /** + * Where the native projects are generated, relative to the project root. + * Defaults to `platforms`. + */ + buildPath?: string; shared?: boolean; overridePods?: string; webpackConfigPath?: string; @@ -191,167 +207,19 @@ interface INsConfig { ignoredNativeDependencies?: string[]; hooks?: INsConfigHooks[]; projectName?: string; -} - -interface IProjectData extends ICreateProjectData { - platformsDir: string; - projectFilePath: string; - projectId: string; - projectIdentifiers?: Mobile.IProjectIdentifier; - dependencies: any; - ignoredDependencies?: string[]; - devDependencies: IStringDictionary; - appDirectoryPath: string; - appResourcesDirectoryPath: string; - projectType: string; - packageJsonData: any; - nsConfig: INsConfig; - androidManifestPath: string; - appGradlePath: string; - gradleFilesDirectoryPath: string; - infoPlistPath: string; - buildXcconfigPath: string; - podfilePath: string; - initialized?: boolean; - /** - * Defines if the project is a code sharing one. - * Value is true when project has nativescript.config and it has `shared: true` in it. - */ - isShared: boolean; - /** - * Specifies the bundler used to build the application. - * - * - `"webpack"`: Uses Webpack for traditional bundling. - * - `"rspack"`: Uses Rspack for fast bundling. - * - `"vite"`: Uses Vite for fast bundling. - * - * @default "webpack" - */ - bundler: BundlerType; - /** - * @deprecated Use bundlerConfigPath - * Defines the path to the configuration file passed to webpack process. - * By default this is the webpack.config.js at the root of the application. - * The value can be changed by setting `webpackConfigPath` in nativescript.config. - */ - webpackConfigPath: string; - /** - * Defines the path to the bundler configuration file passed to the compiler. - * The value can be changed by setting `bundlerConfigPath` in nativescript.config. - */ - bundlerConfigPath: string; - projectName: string; - /** - * Initializes project data with the given project directory. If none supplied defaults to --path option or cwd. - * @param {string} projectDir Project root directory. - * @returns {void} + * Legacy keys still found in user configs. Declared so the + * runtime-package.json generation (PrepareController) can strip them + * via destructuring without losing type safety. */ - initializeProjectData(projectDir?: string): void; - initializeProjectDataFromContent( - packageJsonContent: string, - projectDir?: string, - ): void; - getAppDirectoryPath(projectDir?: string): string; - getAppDirectoryRelativePath(): string; - getAppResourcesDirectoryPath(projectDir?: string): string; - getAppResourcesRelativeDirectoryPath(): string; + webpackPackageName?: string; } -interface IProjectDataService { - /** - * Returns a value from `nativescript` key in project's package.json. - * @param {string} projectDir The project directory - the place where the root package.json is located. - * @param {string} propertyName The name of the property to be checked in `nativescript` key. - * @returns {any} The value of the property. - */ - getNSValue(projectDir: string, propertyName: string): any; - - /** - * Sets a value in the `nativescript` key in a project's package.json. - * @param {string} projectDir The project directory - the place where the root package.json is located. - * @param {string} key Key to be added to `nativescript` key in project's package.json. - * @param {any} value Value of the key to be added to `nativescript` key in project's package.json. - * @returns {void} - */ - setNSValue(projectDir: string, key: string, value: any): void; - - /** - * Removes a property from `nativescript` key in project's package.json. - * @param {string} projectDir The project directory - the place where the root package.json is located. - * @param {string} propertyName The name of the property to be removed from `nativescript` key. - * @returns {void} - */ - removeNSProperty(projectDir: string, propertyName: string): void; - - /** - * Removes a property from `nativescript.config`. - * @param {string} projectDir The project directory - the place where the `nativescript.config` is located. - * @param {string} propertyName The name of the property to be removed. - * @returns {void} - */ - removeNSConfigProperty(projectDir: string, propertyName: string): void; - - /** - * Removes dependency from package.json - * @param {string} projectDir The project directory - the place where the root package.json is located. - * @param {string} dependencyName Name of the dependency that has to be removed. - * @returns {void} - */ - removeDependency(projectDir: string, dependencyName: string): void; - - getProjectData(projectDir?: string): IProjectData; - - /** - * Gives information about the whole assets structure for both iOS and Android. - * For each of the platforms, the returned object will contain icons, splashBackgrounds, splashCenterImages and splashImages (only for iOS). - * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. - * @returns {Promise} An object describing the current asset structure. - */ - getAssetsStructure(opts: IProjectDir): Promise; - - /** - * Gives information about the whole assets structure for iOS. - * The returned object will contain icons, splashBackgrounds, splashCenterImages and splashImages. - * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. - * @returns {Promise} An object describing the current asset structure for iOS. - */ - getIOSAssetsStructure(opts: IProjectDir): Promise; - - /** - * Gives information about the whole assets structure for Android. - * The returned object will contain icons, splashBackgrounds and splashCenterImages. - * @param {IProjectDir} opts Object with a single property - projectDir. This is the root directory where NativeScript project is located. - * @returns {Promise} An object describing the current asset structure for Android. - */ - getAndroidAssetsStructure(opts: IProjectDir): Promise; +/** @deprecated Kept so existing annotations compile; use the {@link ProjectData} contract. */ +interface IProjectData extends ProjectData {} - /** - * Returns array with paths to all `.js` or `.ts` files in application's app directory. - * @param {string} projectDir Path to application. - * @returns {string[]} Array of paths to `.js` or `.ts` files. - */ - getAppExecutableFiles(projectDir: string): string[]; - - /** - * Returns package details for runtime, respecting the nativescript key for legacy projects - * @param {string} projectDir Path to application. - * @param {string} platform Platform key - */ - getRuntimePackage( - projectDir: string, - platform: SupportedPlatform, - ): IBasePluginData; - - /** - * Returns a value from `nativescript` key in project's package.json. - * @param {string} jsonData The project directory - the place where the root package.json is located. - * @param {string} propertyName The name of the property to be checked in `nativescript` key. - * @returns {any} The value of the property. - * @deprecated no longer used - will be removed in 8.0. - */ - getNSValueFromContent(jsonData: Object, propertyName: string): any; -} +/** @deprecated Kept so existing annotations compile; use the {@link ProjectDataService} contract. */ +interface IProjectDataService extends ProjectDataService {} interface IProjectCleanupService { /** @@ -432,9 +300,15 @@ interface IProjectConfigInformation { interface IProjectConfigService { /** * read the nativescript.config.(js|ts) file + * @param options.suppressWarnings pass when reading a config that is not + * the user's project (e.g. a plugin package, which may legitimately ship + * compiled .js artifacts next to its .ts config) * @returns {INsConfig} the parsed config data */ - readConfig(projectDir?: string): INsConfig; + readConfig( + projectDir?: string, + options?: { suppressWarnings?: boolean }, + ): INsConfig; /** * Get value for a given config key path * @param key the property key path @@ -463,7 +337,10 @@ interface IProjectConfigService { */ setForceUsingLegacyConfig(force: boolean): boolean; - detectProjectConfigs(projectDir?: string): IProjectConfigInformation; + detectProjectConfigs( + projectDir?: string, + options?: { suppressWarnings?: boolean }, + ): IProjectConfigInformation; getDefaultTSConfig(appId: string, appPath: string): string; @@ -601,9 +478,7 @@ interface INativePrepare { } interface IBuildConfig - extends IAndroidBuildOptionsSettings, - IiOSBuildConfig, - IProjectDir { + extends IAndroidBuildOptionsSettings, IiOSBuildConfig, IProjectDir { clean?: boolean; architectures?: string[]; buildOutputStdio?: string; @@ -615,7 +490,8 @@ interface IBuildConfig * Describes iOS-specific build configuration properties */ interface IiOSBuildConfig - extends IBuildForDevice, + extends + IBuildForDevice, IiCloudContainerEnvironment, IDeviceIdentifier, IProvision, @@ -631,30 +507,6 @@ interface IiOSBuildConfig codeSignIdentity?: string; } -/** - * Describes service used for building a project locally. - */ -interface ILocalBuildService { - /** - * Builds a project locally. - * @param {string} platform Platform for which to build. - * @param {IPlatformBuildData} platformBuildOptions Additional options for controlling the build. - * @return {Promise} Path to the build output. - */ - build( - platform: string, - platformBuildOptions: IPlatformBuildData, - ): Promise; - /** - * Removes build artifacts specific to the platform - * @param {ICleanNativeAppData} data Data describing the clean app process - * @returns {void} - */ - cleanNativeApp(data: ICleanNativeAppData): Promise; -} - -interface ICleanNativeAppData extends IProjectDir, IPlatform {} - interface IValidatePlatformOutput { checkEnvironmentRequirementsOutput: ICheckEnvironmentRequirementsOutput; } @@ -668,6 +520,12 @@ interface ITestExecutionService { canStartKarmaServer(projectData: IProjectData): Promise; } +interface IVitestExecutionService { + isVitestProject(projectData: IProjectData): boolean; + canStartTestRun(projectData: IProjectData): boolean; + startTestRun(platform: string, projectData: IProjectData): Promise; +} + interface ITestInitializationService { getDependencies(framework: string): IDependencyInformation[]; getFrameworkNames(): string[]; @@ -795,11 +653,6 @@ interface ICocoaPodsPlatformManager { ): { replacedContent: string; podfilePlatformData: IPodfilePlatformData }; } -declare const enum BuildNames { - debug = "Debug", - release = "Release", -} - interface IXcodeTargetBuildConfigurationProperty { name: string; value: any; @@ -865,6 +718,7 @@ interface IAddExtensionsFromPathOptions extends IAddTargetFromPathOptions { interface IAddWatchAppFromPathOptions extends IAddTargetFromPathOptions { watchAppFolderPath: string; + disableStubBinary?: boolean; } interface IRemoveExtensionsOptions { @@ -873,6 +727,37 @@ interface IRemoveExtensionsOptions { interface IRemoveWatchAppOptions extends IRemoveExtensionsOptions {} +interface IWatchAppJSONConfigModule { + name?: string; + path: string; + targetType?: string; + embed?: boolean; + frameworks?: Array>; + dependencies?: string[]; + headerSearchPaths?: string[]; + resources?: string[]; + src?: string[]; + linkerFlags?: string[]; + buildConfigurationProperties?: Record; + SPMPackages?: Array; +} +interface IWatchAppJSONConfig { + targetType?: string; + forceAddEmbedWatchContent?: boolean; + sharedModulesBuildConfigurationProperties?: Record; + basedir?: string; + infoPlistPath?: string; + xcprivacyPath?: string; + importSourcesFromMainFolder?: boolean; + importResourcesFromMainFolder?: boolean; + resources?: string[]; + src?: string[]; + resourcesExclude?: string[]; + srcExclude?: string[]; + modules: IWatchAppConfigModule[]; + SPMPackages?: Array; +} + interface IRubyFunction { functionName: string; functionParameters?: string; diff --git a/lib/definitions/prompter.d.ts b/lib/definitions/prompter.d.ts index abfbea86e6..7247396787 100644 --- a/lib/definitions/prompter.d.ts +++ b/lib/definitions/prompter.d.ts @@ -1,27 +1,6 @@ -import { - IPrompterOptions, - IAllowEmpty, - IDisposable, - IPrompterQuestion, -} from "../common/declarations"; +import type { Prompter } from "../contracts/prompter"; declare global { - interface IPrompter extends IDisposable { - get(schemas: IPrompterQuestion[]): Promise; - getPassword(prompt: string, options?: IAllowEmpty): Promise; - getString(prompt: string, options?: IPrompterOptions): Promise; - promptForChoice( - promptMessage: string, - choices: - | string[] - | { title: string; description?: string; value?: string }[], - multiple: boolean = false, - options: any = {} - ): Promise; - promptForDetailedChoice( - promptMessage: string, - choices: { key: string; description: string }[] - ): Promise; - confirm(prompt: string, defaultAction?: () => boolean): Promise; - } + /** @deprecated Kept so existing annotations compile; use the {@link Prompter} contract. */ + interface IPrompter extends Prompter {} } diff --git a/lib/definitions/qr-code.d.ts b/lib/definitions/qr-code.d.ts deleted file mode 100644 index afdfd3a9c1..0000000000 --- a/lib/definitions/qr-code.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface IQrCodeTerminalService { - generate(url: string): void; -} diff --git a/lib/definitions/system-warnings.d.ts b/lib/definitions/system-warnings.d.ts deleted file mode 100644 index 9985675340..0000000000 --- a/lib/definitions/system-warnings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare const enum SystemWarningsSeverity { - medium = "medium", - high = "high", -} diff --git a/lib/definitions/system-warnings.ts b/lib/definitions/system-warnings.ts new file mode 100644 index 0000000000..b6a53c1962 --- /dev/null +++ b/lib/definitions/system-warnings.ts @@ -0,0 +1,4 @@ +export enum SystemWarningsSeverity { + medium = "medium", + high = "high", +} diff --git a/lib/definitions/temp-service.d.ts b/lib/definitions/temp-service.d.ts index 24038d9ec3..9a5f633ad8 100644 --- a/lib/definitions/temp-service.d.ts +++ b/lib/definitions/temp-service.d.ts @@ -1,3 +1,5 @@ +import type { TempService } from "../contracts/temp-service"; + export type AffixOptions = { prefix?: string; suffix?: string; @@ -7,7 +9,5 @@ export type AffixOptions = { /** * Declares wrapped functions of temp module */ -export interface ITempService { - mkdirSync(affixes: string | AffixOptions): Promise; - path(options: string | AffixOptions): Promise; -} +/** @deprecated Kept so existing annotations compile; use the {@link TempService} contract. */ +export interface ITempService extends TempService {} diff --git a/lib/detached-processes/cleanup-js-subprocess.ts b/lib/detached-processes/cleanup-js-subprocess.ts index e5a5000077..9c262d2c4c 100644 --- a/lib/detached-processes/cleanup-js-subprocess.ts +++ b/lib/detached-processes/cleanup-js-subprocess.ts @@ -6,6 +6,7 @@ import * as fs from "fs"; import { v4 as uuidv4 } from "uuid"; import { FileLogService } from "./file-log-service"; import { injector } from "../common/yok"; +import { FileLogMessageType } from "./detached-process-enums"; const pathToBootstrap = process.argv[2]; if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { @@ -51,28 +52,28 @@ const logMessage = (msg: string, type?: FileLogMessageType): void => { try { logMessage( `Passing data: ${JSON.stringify( - data - )} to the default function exported by currently required file ${jsFilePath}` + data, + )} to the default function exported by currently required file ${jsFilePath}`, ); await func(data); logMessage( `Finished execution with data: ${JSON.stringify( - data - )} to the default function exported by currently required file ${jsFilePath}` + data, + )} to the default function exported by currently required file ${jsFilePath}`, ); } catch (err) { logMessage( `Unable to execute action of file ${jsFilePath} when passed data is ${JSON.stringify( - data + data, )}. Error is: ${err}.`, - FileLogMessageType.Error + FileLogMessageType.Error, ); } } } catch (err) { logMessage( `Unable to require file: ${jsFilePath}. Error is: ${err}.`, - FileLogMessageType.Error + FileLogMessageType.Error, ); } })(); diff --git a/lib/detached-processes/cleanup-process.ts b/lib/detached-processes/cleanup-process.ts index 514766d7ba..2e0e9501b0 100644 --- a/lib/detached-processes/cleanup-process.ts +++ b/lib/detached-processes/cleanup-process.ts @@ -17,6 +17,11 @@ import { IFileCleanupMessage, } from "./cleanup-process-definitions"; import { Server, IChildProcess } from "../common/declarations"; +import { + CleanupProcessMessage, + DetachedProcessMessages, + FileLogMessageType, +} from "./detached-process-enums"; const pathToBootstrap = process.argv[2]; if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { @@ -81,7 +86,7 @@ const executeJSCleanup = async (jsCommand: IJSCommand) => { JSON.stringify(jsCommand.data), ], {}, - { throwError: true, timeout: jsCommand.timeout || 3000 } + { throwError: true, timeout: jsCommand.timeout || 3000 }, ); fileLogService.logData({ message: `Finished executing action for file: ${ @@ -119,17 +124,17 @@ const executeCleanup = async () => { commandInfo.command, commandInfo.args, commandInfo.options || {}, - { throwError: true, timeout: commandInfo.timeout || 3000 } + { throwError: true, timeout: commandInfo.timeout || 3000 }, ); fileLogService.logData({ message: `Successfully executed command: ${JSON.stringify( - commandInfo + commandInfo, )}`, }); } catch (err) { fileLogService.logData({ message: `Unable to execute command: ${JSON.stringify( - commandInfo + commandInfo, )}. Error is: ${err}.`, type: FileLogMessageType.Error, }); @@ -145,7 +150,7 @@ const executeCleanup = async () => { } catch (err) { fileLogService.logData({ message: `Unable to delete files: ${JSON.stringify( - filesToDelete + filesToDelete, )}. Error is: ${err}.`, type: FileLogMessageType.Error, }); @@ -159,18 +164,18 @@ const executeCleanup = async () => { const addCleanupAction = (commandInfo: ISpawnCommandInfo): void => { if ( _.some(commandsInfos, (currentCommandInfo) => - _.isEqual(currentCommandInfo, commandInfo) + _.isEqual(currentCommandInfo, commandInfo), ) ) { fileLogService.logData({ message: `cleanup-process will not add command for execution as it has been added already: ${JSON.stringify( - commandInfo + commandInfo, )}`, }); } else { fileLogService.logData({ message: `cleanup-process added command for execution: ${JSON.stringify( - commandInfo + commandInfo, )}`, }); commandsInfos.push(commandInfo); @@ -180,21 +185,21 @@ const addCleanupAction = (commandInfo: ISpawnCommandInfo): void => { const removeCleanupAction = (commandInfo: ISpawnCommandInfo): void => { if ( _.some(commandsInfos, (currentCommandInfo) => - _.isEqual(currentCommandInfo, commandInfo) + _.isEqual(currentCommandInfo, commandInfo), ) ) { _.remove(commandsInfos, (currentCommandInfo) => - _.isEqual(currentCommandInfo, commandInfo) + _.isEqual(currentCommandInfo, commandInfo), ); fileLogService.logData({ message: `cleanup-process removed command for execution: ${JSON.stringify( - commandInfo + commandInfo, )}`, }); } else { fileLogService.logData({ message: `cleanup-process cannot remove command for execution as it has not been added before: ${JSON.stringify( - commandInfo + commandInfo, )}`, }); } @@ -203,18 +208,18 @@ const removeCleanupAction = (commandInfo: ISpawnCommandInfo): void => { const addRequest = (requestInfo: IRequestInfo): void => { if ( _.some(requests, (currentRequestInfo) => - _.isEqual(currentRequestInfo, requestInfo) + _.isEqual(currentRequestInfo, requestInfo), ) ) { fileLogService.logData({ message: `cleanup-process will not add request for execution as it has been added already: ${JSON.stringify( - requestInfo + requestInfo, )}`, }); } else { fileLogService.logData({ message: `cleanup-process added request for execution: ${JSON.stringify( - requestInfo + requestInfo, )}`, }); requests.push(requestInfo); @@ -224,21 +229,21 @@ const addRequest = (requestInfo: IRequestInfo): void => { const removeRequest = (requestInfo: IRequestInfo): void => { if ( _.some(requests, (currentRequestInfo) => - _.isEqual(currentRequestInfo, currentRequestInfo) + _.isEqual(currentRequestInfo, currentRequestInfo), ) ) { _.remove(requests, (currentRequestInfo) => - _.isEqual(currentRequestInfo, requestInfo) + _.isEqual(currentRequestInfo, requestInfo), ); fileLogService.logData({ message: `cleanup-process removed request for execution: ${JSON.stringify( - requestInfo + requestInfo, )}`, }); } else { fileLogService.logData({ message: `cleanup-process cannot remove request for execution as it has not been added before: ${JSON.stringify( - requestInfo + requestInfo, )}`, }); } @@ -281,18 +286,18 @@ const addJSFile = (jsCommand: IJSCommand): void => { if ( _.some(jsCommands, (currentJSCommand) => - _.isEqual(currentJSCommand, jsCommand) + _.isEqual(currentJSCommand, jsCommand), ) ) { fileLogService.logData({ message: `cleanup-process will not add JS file for execution as it has been added already: ${JSON.stringify( - jsCommand + jsCommand, )}`, }); } else { fileLogService.logData({ message: `cleanup-process added JS file for execution: ${JSON.stringify( - jsCommand + jsCommand, )}`, }); jsCommands.push(jsCommand); @@ -306,21 +311,21 @@ const removeJSFile = (jsCommand: IJSCommand): void => { if ( _.some(jsCommands, (currentJSCommand) => - _.isEqual(currentJSCommand, jsCommand) + _.isEqual(currentJSCommand, jsCommand), ) ) { _.remove(jsCommands, (currentJSCommand) => - _.isEqual(currentJSCommand, jsCommand) + _.isEqual(currentJSCommand, jsCommand), ); fileLogService.logData({ message: `cleanup-process removed JS action for execution: ${JSON.stringify( - jsCommand + jsCommand, )}`, }); } else { fileLogService.logData({ message: `cleanup-process cannot remove JS action for execution as it has not been added before: ${JSON.stringify( - jsCommand + jsCommand, )}`, }); } @@ -329,19 +334,19 @@ const removeJSFile = (jsCommand: IJSCommand): void => { process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => { fileLogService.logData({ message: `cleanup-process received message of type: ${JSON.stringify( - cleanupProcessMessage + cleanupProcessMessage, )}`, }); switch (cleanupProcessMessage.messageType) { case CleanupProcessMessage.AddCleanCommand: addCleanupAction( - (cleanupProcessMessage).commandInfo + (cleanupProcessMessage).commandInfo, ); break; case CleanupProcessMessage.RemoveCleanCommand: removeCleanupAction( - (cleanupProcessMessage).commandInfo + (cleanupProcessMessage).commandInfo, ); break; case CleanupProcessMessage.AddRequest: @@ -349,7 +354,7 @@ process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => { break; case CleanupProcessMessage.RemoveRequest: removeRequest( - (cleanupProcessMessage).requestInfo + (cleanupProcessMessage).requestInfo, ); break; case CleanupProcessMessage.AddDeleteFileAction: diff --git a/lib/detached-processes/detached-process-enums.d.ts b/lib/detached-processes/detached-process-enums.ts similarity index 93% rename from lib/detached-processes/detached-process-enums.d.ts rename to lib/detached-processes/detached-process-enums.ts index 13e11a78a3..4c6695ac60 100644 --- a/lib/detached-processes/detached-process-enums.d.ts +++ b/lib/detached-processes/detached-process-enums.ts @@ -1,7 +1,7 @@ /** * Defines messages used in communication between CLI's process and analytics subprocesses. */ -declare const enum DetachedProcessMessages { +export enum DetachedProcessMessages { /** * The detached process is initialized and is ready to receive information for tracking. */ @@ -16,7 +16,7 @@ declare const enum DetachedProcessMessages { /** * Defines the type of the messages that should be written in the local analyitcs log file (in case such is specified). */ -declare const enum FileLogMessageType { +export enum FileLogMessageType { /** * Information message. This is the default value in case type is not specified. */ @@ -28,7 +28,7 @@ declare const enum FileLogMessageType { Error = "Error", } -declare const enum CleanupProcessMessage { +export enum CleanupProcessMessage { /** * This type of message defines that cleanup procedure should execute specific command. */ diff --git a/lib/detached-processes/file-log-service.ts b/lib/detached-processes/file-log-service.ts index 6684784842..5ab23aee37 100644 --- a/lib/detached-processes/file-log-service.ts +++ b/lib/detached-processes/file-log-service.ts @@ -1,9 +1,13 @@ import { EOL } from "os"; import { getFixedLengthDateString } from "../common/helpers"; import { IFileSystem } from "../common/declarations"; +import { FileLogMessageType } from "./detached-process-enums"; export class FileLogService implements IFileLogService { - constructor(private $fs: IFileSystem, private logFile: string) {} + constructor( + private $fs: IFileSystem, + private logFile: string, + ) {} public logData(fileLoggingMessage: IFileLogMessage): void { if (this.logFile && fileLoggingMessage && fileLoggingMessage.message) { @@ -12,7 +16,7 @@ export class FileLogService implements IFileLogService { const formattedDate = getFixedLengthDateString(); this.$fs.appendFile( this.logFile, - `[${formattedDate}] [${fileLoggingMessage.type}] ${fileLoggingMessage.message}${EOL}` + `[${formattedDate}] [${fileLoggingMessage.type}] ${fileLoggingMessage.message}${EOL}`, ); } } diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index ea9cc44ea4..432b931e76 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -1,10 +1,10 @@ import * as _ from "lodash"; import { - ErrorCodes, IAnalyticsService, IDictionary, IErrors, } from "../common/declarations"; +import { ErrorCodes } from "../common/enums"; import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import { RunOnDeviceEvents } from "../constants"; @@ -31,7 +31,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { private $errors: IErrors, private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider, private $cleanupService: ICleanupService, - private $runController: IRunController + private $runController: IRunController, ) {} private get $platformsDataService(): IPlatformsDataService { @@ -56,7 +56,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { } public async getDeviceInstances( - platform?: string + platform?: string, ): Promise { await this.$devicesService.initialize({ platform, @@ -71,7 +71,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { .filter( (d) => !platform || - d.deviceInfo.platform.toLowerCase() === platform.toLowerCase() + d.deviceInfo.platform.toLowerCase() === platform.toLowerCase(), ); return devices; @@ -80,7 +80,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { public async createDeviceDescriptors( devices: Mobile.IDevice[], platform: string, - additionalOptions?: ILiveSyncCommandHelperAdditionalOptions + additionalOptions?: ILiveSyncCommandHelperAdditionalOptions, ): Promise { // Now let's take data for each device: const deviceDescriptors: ILiveSyncDeviceDescriptor[] = devices.map((d) => { @@ -105,7 +105,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { forceRebuildNativeApp: additionalOptions.forceRebuildNativeApp, }, _device: d, - } + }, ); this.$androidBundleValidatorHelper.validateDeviceApiLevel(d, buildData); @@ -115,8 +115,8 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { additionalOptions.buildPlatform, d.deviceInfo.platform, buildData, - this.$projectData - ) + this.$projectData, + ) : this.$buildController.build.bind(this.$buildController, buildData); const info: ILiveSyncDeviceDescriptor = { @@ -142,14 +142,14 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { const availablePlatforms = platform ? [platform] : _.values( - this.$mobileHelper.platformNames.map((p) => p.toLowerCase()) - ); + this.$mobileHelper.platformNames.map((p) => p.toLowerCase()), + ); return availablePlatforms; } public async executeCommandLiveSync( platform?: string, - additionalOptions?: ILiveSyncCommandHelperAdditionalOptions + additionalOptions?: ILiveSyncCommandHelperAdditionalOptions, ) { const devices = await this.getDeviceInstances(platform); await this.executeLiveSyncOperation(devices, platform, additionalOptions); @@ -158,13 +158,13 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { public async executeLiveSyncOperation( devices: Mobile.IDevice[], platform: string, - additionalOptions?: ILiveSyncCommandHelperAdditionalOptions + additionalOptions?: ILiveSyncCommandHelperAdditionalOptions, ): Promise { const { liveSyncInfo, deviceDescriptors } = await this.executeLiveSyncOperationCore( devices, platform, - additionalOptions + additionalOptions, ); if (this.$options.release) { @@ -204,19 +204,19 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { }) => { const devices = await this.getDeviceInstances(platform); const remainingDevicesToSync = devices.map( - (d) => d.deviceInfo.identifier + (d) => d.deviceInfo.identifier, ); _.remove(remainingDevicesToSync, (d) => d === data.deviceIdentifier); if (remainingDevicesToSync.length === 0 && !data.keepProcessAlive) { process.exit(ErrorCodes.ALL_DEVICES_DISCONNECTED); } - } + }, ); } public async validatePlatform( - platform: string + platform: string, ): Promise> { const result: IDictionary = {}; @@ -224,12 +224,12 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { for (const availablePlatform of availablePlatforms) { const platformData = this.$platformsDataService.getPlatformData( availablePlatform, - this.$projectData + this.$projectData, ); const platformProjectService = platformData.platformProjectService; const validateOutput = await platformProjectService.validate( this.$projectData, - this.$options + this.$options, ); result[availablePlatform.toLowerCase()] = validateOutput; } @@ -240,7 +240,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { private async executeLiveSyncOperationCore( devices: Mobile.IDevice[], platform: string, - additionalOptions?: ILiveSyncCommandHelperAdditionalOptions + additionalOptions?: ILiveSyncCommandHelperAdditionalOptions, ): Promise<{ liveSyncInfo: ILiveSyncInfo; deviceDescriptors: ILiveSyncDeviceDescriptor[]; @@ -248,11 +248,11 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { if (!devices || !devices.length) { if (platform) { this.$errors.fail( - "Unable to find applicable devices to execute operation. Ensure connected devices are trusted and try again." + "Unable to find applicable devices to execute operation. Ensure connected devices are trusted and try again.", ); } else { this.$errors.fail( - "Unable to find applicable devices to execute operation and unable to start emulator when platform is not specified." + "Unable to find applicable devices to execute operation and unable to start emulator when platform is not specified.", ); } } @@ -273,7 +273,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { const deviceDescriptors = await this.createDeviceDescriptors( devices, platform, - additionalOptions + additionalOptions, ); const liveSyncInfo = this.getLiveSyncData(this.$projectData.projectDir); @@ -282,7 +282,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { private async runInRelease( platform: string, - deviceDescriptors: ILiveSyncDeviceDescriptor[] + deviceDescriptors: ILiveSyncDeviceDescriptor[], ): Promise { await this.$devicesService.initialize({ platform, @@ -296,7 +296,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { for (const deviceDescriptor of deviceDescriptors) { const device = this.$devicesService.getDeviceByIdentifier( - deviceDescriptor.identifier + deviceDescriptor.identifier, ); await device.applicationManager.startApplication({ appId: diff --git a/lib/helpers/network-connectivity-validator.ts b/lib/helpers/network-connectivity-validator.ts deleted file mode 100644 index 05cb042596..0000000000 --- a/lib/helpers/network-connectivity-validator.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as dns from "dns"; -import { INetworkConnectivityValidator } from "../declarations"; -import { injector } from "../common/yok"; -import { IErrors } from "../common/declarations"; - -export class NetworkConnectivityValidator - implements INetworkConnectivityValidator { - private static DNS_LOOKUP_URL = "play.nativescript.org"; - private static NO_INTERNET_ERROR_CODE = "ENOTFOUND"; - private static NO_INTERNET_ERROR_MESSAGE = - "No internet connection. Check your internet settings and try again."; - - constructor(private $errors: IErrors, private $logger: ILogger) {} - - public async validate(): Promise { - const isConnected = await this.isConnected(); - if (!isConnected) { - this.$errors.fail(NetworkConnectivityValidator.NO_INTERNET_ERROR_MESSAGE); - } - } - - private isConnected(): Promise { - return new Promise((resolve, reject) => { - dns.lookup(NetworkConnectivityValidator.DNS_LOOKUP_URL, (err) => { - this.$logger.trace(`Error from dns.lookup is ${err}.`); - if ( - err && - err.code === NetworkConnectivityValidator.NO_INTERNET_ERROR_CODE - ) { - resolve(false); - } else { - resolve(true); - } - }); - }); - } -} -injector.register("networkConnectivityValidator", NetworkConnectivityValidator); diff --git a/lib/helpers/options-track-helper.ts b/lib/helpers/options-track-helper.ts index cebcb12ae6..2a098d8c3f 100644 --- a/lib/helpers/options-track-helper.ts +++ b/lib/helpers/options-track-helper.ts @@ -1,16 +1,16 @@ import * as path from "path"; import { TrackActionNames } from "../constants"; -import { IOptions } from "../declarations"; +import { IOptions, IOptionsTracker } from "../declarations"; import { IAnalyticsService, IDictionary, IDashedOption, - OptionType, } from "../common/declarations"; +import { OptionType } from "../common/enums"; import * as _ from "lodash"; import { injector } from "../common/yok"; -export class OptionsTracker { +export class OptionsTracker implements IOptionsTracker { public static PASSWORD_DETECTION_STRING = "password"; public static PRIVATE_REPLACE_VALUE = "private"; public static PATH_REPLACE_VALUE = "_localpath"; @@ -35,7 +35,7 @@ export class OptionsTracker { private sanitizeTrackObject( data: IDictionary, - options?: IOptions + options?: IOptions, ): IDictionary { const shorthands = options ? options.shorthands : []; const optionsDefinitions = options ? options.options : {}; @@ -72,7 +72,7 @@ export class OptionsTracker { key: string, value: any, shorthands: string[] = [], - options: IDictionary = {} + options: IDictionary = {}, ): Boolean { if (shorthands.indexOf(key) >= 0) { return true; diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index 04b2f90fd3..cf122f948c 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -35,7 +35,8 @@ if (process.platform === "win32") { import { installUncaughtExceptionListener } from "./common/errors"; import { settlePromises } from "./common/helpers"; import { injector } from "./common/yok"; -import { ErrorCodes, IErrors, ICommandDispatcher } from "./common/declarations"; +import { IErrors, ICommandDispatcher } from "./common/declarations"; +import { ErrorCodes } from "./common/enums"; import { IExtensibilityService, IExtensionData, diff --git a/lib/options.ts b/lib/options.ts index d5dcd1b509..df6ce62f4d 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -5,25 +5,17 @@ import * as _ from "lodash"; import { IDictionary, IDashedOption, - OptionType, IErrors, ISettingsService, } from "./common/declarations"; +import { OptionType } from "./common/enums"; import { injector } from "./common/yok"; import { APP_FOLDER_NAME } from "./constants"; export class Options { private static DASHED_OPTION_REGEX = /(.+?)([A-Z])(.*)/; private static NONDASHED_OPTION_REGEX = /(.+?)[-]([a-zA-Z])(.*)/; - private optionsWhiteList = [ - "ui", - "recursive", - "reporter", - "require", - "timeout", - "_", - "$0", - ]; // These options shouldn't be validated + private optionsWhiteList = ["_", "$0"]; // yargs artifacts, not options private globalOptions: IDictionary = { log: { type: OptionType.String, hasSensitiveValue: false }, verbose: { type: OptionType.Boolean, hasSensitiveValue: false }, @@ -42,7 +34,7 @@ export class Options { public options: IDictionary; public setupOptions( - commandSpecificDashedOptions?: IDictionary + commandSpecificDashedOptions?: IDictionary, ): void { if (commandSpecificDashedOptions) { _.extend(this.options, commandSpecificDashedOptions); @@ -54,7 +46,7 @@ export class Options { // Check if the user has explicitly provide --hmr and --release options from command line if (this.initialArgv.release && this.initialArgv.hmr) { this.$errors.fail( - "The options --release and --hmr cannot be used simultaneously." + "The options --release and --hmr cannot be used simultaneously.", ); } @@ -75,7 +67,8 @@ export class Options { constructor( private $errors: IErrors, - private $settingsService: ISettingsService + private $settingsService: ISettingsService, + private $logger: ILogger, ) { this.options = _.extend({}, this.commonOptions, this.globalOptions); this.setArgv(); @@ -84,8 +77,9 @@ export class Options { public get shorthands(): string[] { const result: string[] = []; _.each(_.keys(this.options), (optionName) => { - if (this.options[optionName].alias) { - result.push(this.options[optionName].alias); + const alias = this.options[optionName].alias; + if (alias) { + result.push(...(_.isArray(alias) ? alias : [alias])); } }); return result; @@ -133,6 +127,7 @@ export class Options { vue: { type: OptionType.Boolean, hasSensitiveValue: false }, vuejs: { type: OptionType.Boolean, hasSensitiveValue: false }, svelte: { type: OptionType.Boolean, hasSensitiveValue: false }, + solid: { type: OptionType.Boolean, hasSensitiveValue: false }, vision: { type: OptionType.Boolean, hasSensitiveValue: false }, "vision-ng": { type: OptionType.Boolean, hasSensitiveValue: false }, "vision-react": { type: OptionType.Boolean, hasSensitiveValue: false }, @@ -177,8 +172,6 @@ export class Options { }, json: { type: OptionType.Boolean, hasSensitiveValue: false }, avd: { type: OptionType.String, hasSensitiveValue: true }, - // check not used - config: { type: OptionType.Array, hasSensitiveValue: false }, insecure: { type: OptionType.Boolean, alias: "k", @@ -226,6 +219,7 @@ export class Options { default: false, hasSensitiveValue: false, }, + gradleFlavor: { type: OptionType.String, hasSensitiveValue: false }, gradlePath: { type: OptionType.String, hasSensitiveValue: false }, gradleArgs: { type: OptionType.String, hasSensitiveValue: false }, hostProjectPath: { type: OptionType.String, hasSensitiveValue: false }, @@ -250,6 +244,7 @@ export class Options { default: true, }, dryRun: { type: OptionType.Boolean, hasSensitiveValue: false }, + skipNative: { type: OptionType.Boolean, hasSensitiveValue: false }, uniqueBundle: { type: OptionType.Boolean, hasSensitiveValue: false }, }; } @@ -264,65 +259,114 @@ export class Options { } public validateOptions( - commandSpecificDashedOptions?: IDictionary + commandSpecificDashedOptions?: IDictionary, ): void { this.setupOptions(commandSpecificDashedOptions); - const parsed: any = {}; - for (const key of Object.keys(this.argv)) { - const optionName = `${this.argv[key]}`; - parsed[optionName] = this.getOptionValue(optionName); - } - _.each(parsed, (value: any, originalOptionName: string) => { - // when this.options are passed to yargs, it returns all of them and the ones that are not part of process.argv are set to undefined. - if (value === undefined) { - return; + const validated: string[] = []; + for (const originalOptionName of Object.keys(this.argv)) { + const optionValue = this.getOptionValue(originalOptionName); + // yargs reports every declared option; the ones that are not part of + // process.argv come back undefined. + if (optionValue === undefined) { + continue; } const optionName = this.getCorrectOptionName(originalOptionName); - if (!_.includes(this.optionsWhiteList, optionName)) { - if (!this.isOptionSupported(optionName)) { - this.$errors.failWithHelp( - `The option '${originalOptionName}' is not supported.` - ); - } + if (_.includes(this.optionsWhiteList, optionName)) { + continue; + } - const optionType = this.getOptionType(optionName), - optionValue = parsed[optionName]; - - if (_.isArray(optionValue) && optionType !== OptionType.Array) { - this.$errors.failWithHelp( - "The '%s' option requires a single value.", - originalOptionName - ); - } else if ( - optionType === OptionType.String && - helpers.isNullOrWhitespace(optionValue) - ) { - this.$errors.failWithHelp( - "The option '%s' requires non-empty value.", - originalOptionName - ); - } else if ( - optionType === OptionType.Array && - optionValue.length === 0 - ) { - this.$errors.failWithHelp( - `The option '${originalOptionName}' requires one or more values, separated by a space.` - ); - } + // yargs emits every spelling of a flag: dashed, camelCase and, for an + // aliased option, the alias. Collapse them so one flag is reported once. + const dedupeKey = this.getCanonicalOptionName(optionName); + if (_.includes(validated, dedupeKey)) { + continue; } - }); + validated.push(dedupeKey); + + if (!this.isOptionSupported(optionName)) { + this.reportInvalidOption( + `The option '${this.getReportedOptionName( + originalOptionName, + )}' is not supported.`, + ); + continue; + } + + const optionType = this.getOptionType(optionName); + + if (_.isArray(optionValue) && optionType !== OptionType.Array) { + this.reportInvalidOption( + `The '${originalOptionName}' option requires a single value.`, + ); + } else if ( + optionType === OptionType.String && + helpers.isNullOrWhitespace(optionValue) + ) { + this.reportInvalidOption( + `The option '${originalOptionName}' requires non-empty value.`, + ); + } else if (optionType === OptionType.Array && optionValue.length === 0) { + this.reportInvalidOption( + `The option '${originalOptionName}' requires one or more values, separated by a space.`, + ); + } + } } - private getCorrectOptionName(optionName: string): string { - const secondaryOptionName = this.getNonDashedOptionName(optionName); - return _.includes(this.optionNames, secondaryOptionName) - ? secondaryOptionName + // The name every spelling of an option collapses to. Unknown options keep + // their own name; there is no declaration to resolve them against. + private getCanonicalOptionName(optionName: string): string { + const correctName = this.getCorrectOptionName(optionName); + if (this.options[correctName]) { + return this.getNonDashedOptionName(correctName); + } + + const aliasedName = _.findKey(this.options, (opt) => + this.hasAlias(opt, correctName), + ); + return this.getNonDashedOptionName(aliasedName || correctName); + } + + // yargs strips the `no-` prefix off a negated flag, so an undeclared + // `--no-foo` surfaces as `foo` and would otherwise be reported under a name + // the user never typed. + private getReportedOptionName(optionName: string): string { + return process.argv.indexOf(`--no-${optionName}`) !== -1 + ? `no-${optionName}` : optionName; } + private reportInvalidOption(message: string): void { + if (process.env.NS_STRICT_OPTIONS === "error") { + this.$errors.failWithHelp(message); + return; + } + + this.$logger.warn( + `${message} This will become an error in a future release. Set NS_STRICT_OPTIONS=error to preview that behavior.`, + ); + } + + private getCorrectOptionName(optionName: string): string { + const nonDashedName = this.getNonDashedOptionName(optionName); + if (_.includes(this.optionNames, nonDashedName)) { + return nonDashedName; + } + + // A few options are declared with a literal dashed key (vision-ng and + // friends). yargs still reports both spellings, so the camelCase one has + // to resolve back to the dashed declaration. + const dashedName = this.getDashedOptionName(optionName); + if (_.includes(this.optionNames, dashedName)) { + return dashedName; + } + + return optionName; + } + private getOptionType(optionName: string): string { const option = this.options[optionName] || this.tryGetOptionByAliasName(optionName); @@ -330,10 +374,18 @@ export class Options { } private tryGetOptionByAliasName(aliasName: string) { - const option = _.find(this.options, (opt) => opt.alias === aliasName); + const option = _.find(this.options, (opt) => this.hasAlias(opt, aliasName)); return option; } + // yargs accepts an option's `alias` as a single string or an array of them, + // so every alias lookup has to cope with both. + private hasAlias(option: IDashedOption, aliasName: string): boolean { + return _.isArray(option.alias) + ? _.includes(option.alias, aliasName) + : option.alias === aliasName; + } + private isOptionSupported(option: string): boolean { if (!this.options[option]) { const opt = this.tryGetOptionByAliasName(option); @@ -350,7 +402,7 @@ export class Options { // This way your code will work in case "$ emulate android --profile-dir" or "$ emulate android --profileDir" is used by user. private getNonDashedOptionName(optionName: string): string { const matchUpperCaseLetters = optionName.match( - Options.NONDASHED_OPTION_REGEX + Options.NONDASHED_OPTION_REGEX, ); if (matchUpperCaseLetters) { // get here if option with upperCase letter is specified, for example profileDir @@ -410,7 +462,7 @@ export class Options { .map((match) => { return match[currentDepth]; }) - .filter(Boolean) + .filter(Boolean), ), ]; diff --git a/lib/project-data.ts b/lib/project-data.ts index 277dbf32d1..124a0eb74e 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -169,14 +169,18 @@ export class ProjectData implements IProjectData { nsConfig && nsConfig.projectName ? nsConfig.projectName : this.$projectHelper.sanitizeName(path.basename(projectDir)); - this.platformsDir = path.join(projectDir, constants.PLATFORMS_DIR_NAME); + // read before `platformsDir`, which is derived from it + this.nsConfig = nsConfig; + this.platformsDir = path.join( + projectDir, + this.getBuildRelativeDirectoryPath(), + ); this.projectFilePath = projectFilePath; this.projectIdentifiers = this.initializeProjectIdentifiers(nsConfig); this.packageJsonData = packageJsonData; this.dependencies = packageJsonData.dependencies; this.devDependencies = packageJsonData.devDependencies; this.projectType = this.getProjectType(); - this.nsConfig = nsConfig; this.ignoredDependencies = nsConfig?.ignoredNativeDependencies; this.appDirectoryPath = this.getAppDirectoryPath(); this.appResourcesDirectoryPath = this.getAppResourcesDirectoryPath(); @@ -276,6 +280,18 @@ export class ProjectData implements IProjectData { // ); } + /** + * Where the native projects are generated, relative to the project root. + * `buildPath` in the project config overrides the default `platforms`. + */ + public getBuildRelativeDirectoryPath(): string { + if (this.nsConfig && this.nsConfig[constants.CONFIG_NS_BUILD_ENTRY]) { + return this.nsConfig[constants.CONFIG_NS_BUILD_ENTRY]; + } + + return constants.PLATFORMS_DIR_NAME; + } + public getAppDirectoryPath(projectDir?: string): string { const appRelativePath = this.getAppDirectoryRelativePath(); diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts index 24e202ac46..970e0579e8 100644 --- a/lib/services/analytics/analytics-broker-process.ts +++ b/lib/services/analytics/analytics-broker-process.ts @@ -6,7 +6,8 @@ import { AnalyticsBroker } from "./analytics-broker"; import { FileLogService } from "../../detached-processes/file-log-service"; import { IAnalyticsBroker, ITrackingInformation } from "./analytics"; import { injector } from "../../common/yok"; -import { TrackingTypes } from "../../common/declarations"; +import { TrackingTypes } from "../../common/enums"; +import { DetachedProcessMessages } from "../../detached-processes/detached-process-enums"; const pathToBootstrap = process.argv[2]; if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts index 8a12dbe4ae..338eb9d52a 100644 --- a/lib/services/analytics/analytics-broker.ts +++ b/lib/services/analytics/analytics-broker.ts @@ -7,12 +7,11 @@ import { } from "./analytics"; import { IAnalyticsSettingsService } from "../../common/declarations"; import { IInjector } from "../../common/definitions/yok"; +import { FileLogMessageType } from "../../detached-processes/detached-process-enums"; export class AnalyticsBroker implements IAnalyticsBroker { @cache() - private async getGoogleAnalyticsProvider(): Promise< - IGoogleAnalyticsProvider - > { + private async getGoogleAnalyticsProvider(): Promise { const clientId = await this.$analyticsSettingsService.getClientId(); return this.$injector.resolve("googleAnalyticsProvider", { clientId, @@ -23,16 +22,16 @@ export class AnalyticsBroker implements IAnalyticsBroker { constructor( private $analyticsSettingsService: IAnalyticsSettingsService, private $injector: IInjector, - private analyticsLoggingService: IFileLogService + private analyticsLoggingService: IFileLogService, ) {} public async sendDataForTracking( - trackInfo: ITrackingInformation + trackInfo: ITrackingInformation, ): Promise { try { const googleProvider = await this.getGoogleAnalyticsProvider(); await googleProvider.trackHit( - trackInfo + trackInfo, ); } catch (err) { this.analyticsLoggingService.logData({ diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index f9b10ee644..6eec0f7aa9 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -11,15 +11,17 @@ import { IAnalyticsService, IDisposable, IDictionary, - AnalyticsStatus, IUserSettingsService, IAnalyticsSettingsService, IChildProcess, IProjectHelper, - GoogleAnalyticsDataType, IStringDictionary, - TrackingTypes, } from "../../common/declarations"; +import { + AnalyticsStatus, + GoogleAnalyticsDataType, + TrackingTypes, +} from "../../common/enums"; import { IGoogleAnalyticsTrackingInformation, ITrackingInformation, @@ -31,6 +33,8 @@ import { IEventActionData, } from "../../common/definitions/google-analytics"; import { injector } from "../../common/yok"; +import { DetachedProcessMessages } from "../../detached-processes/detached-process-enums"; +import { GoogleAnalyticsCustomDimensions } from "../../common/services/analytics/google-analytics-custom-dimensions"; export class AnalyticsService implements IAnalyticsService, IDisposable { private static ANALYTICS_BROKER_START_TIMEOUT = 10 * 1000; diff --git a/lib/services/analytics/google-analytics-cross-client-custom-dimensions.d.ts b/lib/services/analytics/google-analytics-cross-client-custom-dimensions.d.ts deleted file mode 100644 index c58e2b54c0..0000000000 --- a/lib/services/analytics/google-analytics-cross-client-custom-dimensions.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Sync indexes with the custom dimensions of the cross client analytics project -declare const enum GoogleAnalyticsCrossClientCustomDimensions { - sessionId = "cd9", - clientId = "cd10", - crossClientId = "cd12", -} diff --git a/lib/services/analytics/google-analytics-provider.ts b/lib/services/analytics/google-analytics-provider.ts index 3feb0454bf..d756cfb57d 100644 --- a/lib/services/analytics/google-analytics-provider.ts +++ b/lib/services/analytics/google-analytics-provider.ts @@ -1,14 +1,14 @@ import { v4 as uuidv4 } from "uuid"; -import * as ua from "universal-analytics"; import { AnalyticsClients } from "../../common/constants"; -import { cache } from "../../common/decorators"; import { IStaticConfig, IConfiguration } from "../../declarations"; import { IAnalyticsSettingsService, + IDictionary, IProxyService, - GoogleAnalyticsDataType, IStringDictionary, + Server, } from "../../common/declarations"; +import { GoogleAnalyticsDataType } from "../../common/enums"; import { IGoogleAnalyticsProvider } from "./analytics"; import { IGoogleAnalyticsData, @@ -17,9 +17,31 @@ import { } from "../../common/definitions/google-analytics"; import * as _ from "lodash"; import { injector } from "../../common/yok"; +import { FileLogMessageType } from "../../detached-processes/detached-process-enums"; +import { GoogleAnalyticsCustomDimensions } from "../../common/services/analytics/google-analytics-custom-dimensions"; + +const GA4_COLLECT_URL = "https://www.google-analytics.com/mp/collect"; + +// Event and parameter names accept only letters, digits and underscores, must +// lead with a letter, and are truncated past these lengths server-side. +const MAX_EVENT_NAME_LENGTH = 40; +const MAX_PARAM_VALUE_LENGTH = 100; + +// The Measurement Protocol carries named parameters where the classic protocol +// carried numbered cdN slots, so the dimensions are translated on the way out. +// Callers keep setting GoogleAnalyticsCustomDimensions and never see this. +const GA4_PARAM_NAMES: IStringDictionary = { + [GoogleAnalyticsCustomDimensions.cliVersion]: "cli_version", + [GoogleAnalyticsCustomDimensions.projectType]: "project_type", + [GoogleAnalyticsCustomDimensions.clientID]: "client_uuid", + [GoogleAnalyticsCustomDimensions.sessionID]: "session_id", + [GoogleAnalyticsCustomDimensions.client]: "client", + [GoogleAnalyticsCustomDimensions.nodeVersion]: "node_version", + [GoogleAnalyticsCustomDimensions.isShared]: "is_shared", +}; export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { - private currentPage: string; + private currentCommand: string; constructor( private clientId: string, @@ -28,84 +50,131 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { private $logger: ILogger, private $proxyService: IProxyService, private $config: IConfiguration, - private analyticsLoggingService: IFileLogService + private $httpClient: Server.IHttpClient, + private analyticsLoggingService: IFileLogService, ) {} public async trackHit(trackInfo: IGoogleAnalyticsData): Promise { const sessionId = uuidv4(); try { - await this.track(this.$config.GA_TRACKING_ID, trackInfo, sessionId); + await this.track(trackInfo, sessionId); } catch (e) { this.analyticsLoggingService.logData({ type: FileLogMessageType.Error, message: `Unable to track information ${JSON.stringify( - trackInfo + trackInfo, )}. Error is: ${e}`, }); this.$logger.trace("Analytics exception: ", e); } } - @cache() - private getVisitor(gaTrackingId: string, proxy: string): ua.Visitor { + private async track( + trackInfo: IGoogleAnalyticsData, + sessionId: string, + ): Promise { + const { GA_MEASUREMENT_ID, GA_API_SECRET } = this.$config; + + if (!GA_MEASUREMENT_ID || !GA_API_SECRET) { + this.analyticsLoggingService.logData({ + message: + "Google Analytics is not configured (missing measurement id or api secret), skipping hit.", + }); + return; + } + + const event = this.getEvent(trackInfo, sessionId); + + if (!event) { + return; + } + + const proxySettings = await this.$proxyService.getCache(); + const url = `${GA4_COLLECT_URL}?measurement_id=${encodeURIComponent( + GA_MEASUREMENT_ID, + )}&api_secret=${encodeURIComponent(GA_API_SECRET)}`; + this.analyticsLoggingService.logData({ - message: `Initializing Google Analytics visitor for id: ${gaTrackingId} with clientId: ${this.clientId}.`, + message: `Sending Google Analytics event '${event.name}' for clientId: ${this.clientId}.`, }); - const visitor = ua({ - tid: gaTrackingId, - cid: this.clientId, - headers: { - ["User-Agent"]: this.$analyticsSettingsService.getUserAgentString( - `tnsCli/${this.$staticConfig.version}` - ), - }, - requestOptions: { - proxy, + + await this.$httpClient.httpRequest( + { + url, + method: "POST", + headers: { + "Content-Type": "application/json", + ["User-Agent"]: this.$analyticsSettingsService.getUserAgentString( + `tnsCli/${this.$staticConfig.version}`, + ), + }, + body: JSON.stringify({ + client_id: this.clientId, + // the CLI has no advertising context and must not create one + non_personalized_ads: true, + events: [event], + }), }, - https: true, - }); + proxySettings, + ); this.analyticsLoggingService.logData({ - message: `Successfully initialized Google Analytics visitor for id: ${gaTrackingId} with clientId: ${this.clientId}.`, + message: `Tracked Google Analytics event '${event.name}'.`, }); - return visitor; } - private async track( - gaTrackingId: string, + private getEvent( trackInfo: IGoogleAnalyticsData, - sessionId: string - ): Promise { - const proxySettings = await this.$proxyService.getCache(); - const proxy = proxySettings && proxySettings.proxy; - - const visitor = this.getVisitor(gaTrackingId, proxy); - - await this.setCustomDimensions( - visitor, + sessionId: string, + ): { name: string; params: IDictionary } { + const params = this.getCustomDimensionParams( trackInfo.customDimensions, - sessionId + sessionId, ); switch (trackInfo.googleAnalyticsDataType) { - case GoogleAnalyticsDataType.Page: - await this.trackPageView( - visitor, - trackInfo - ); - break; - case GoogleAnalyticsDataType.Event: - await this.trackEvent(visitor, trackInfo); - break; + case GoogleAnalyticsDataType.Page: { + const pageviewData = trackInfo; + this.currentCommand = pageviewData.path; + + // a command is not a page: page_view is keyed off a page_location URL + // this has none, so commands are their own event instead. `title` is + // dropped because callers set it to the same beautified command name. + return { + name: "command", + params: _.assign(params, { + command_name: this.truncate(pageviewData.path), + }), + }; + } + case GoogleAnalyticsDataType.Event: { + const eventData = trackInfo; + + return { + name: this.toEventName(eventData.action), + params: _.omitBy( + _.assign(params, { + event_category: this.truncate(eventData.category), + event_label: this.truncate(eventData.label), + value: eventData.value, + // events carry no context of their own, so attribute them to + // the command that is running + command_name: this.truncate(this.currentCommand), + }), + _.isNil, + ) as IDictionary, + }; + } } + + return null; } - private async setCustomDimensions( - visitor: ua.Visitor, + private getCustomDimensionParams( customDimensions: IStringDictionary, - sessionId: string - ): Promise { + sessionId: string, + ): IDictionary { const defaultValues: IStringDictionary = { [GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version, [GoogleAnalyticsCustomDimensions.nodeVersion]: process.version, @@ -116,78 +185,33 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { [GoogleAnalyticsCustomDimensions.client]: AnalyticsClients.Unknown, }; - customDimensions = _.merge(defaultValues, customDimensions); + const params: IDictionary = { + // realtime reports drop events that report no engagement at all + engagement_time_msec: 1, + }; - _.each(customDimensions, (value, key) => { - this.analyticsLoggingService.logData({ - message: `Setting custom dimension ${key} to value ${value}`, - }); - visitor.set(key, value); + _.each(_.merge(defaultValues, customDimensions), (value, key) => { + if (_.isNil(value)) { + return; + } + + params[GA4_PARAM_NAMES[key] || key] = this.truncate(value); }); + + return params; } - private trackEvent( - visitor: ua.Visitor, - trackInfo: IGoogleAnalyticsEventData - ): Promise { - return new Promise((resolve, reject) => { - visitor.event( - trackInfo.category, - trackInfo.action, - trackInfo.label, - trackInfo.value, - { p: this.currentPage }, - (err: Error) => { - if (err) { - this.analyticsLoggingService.logData({ - message: - `Unable to track event with category: '${trackInfo.category}', action: '${trackInfo.action}', label: '${trackInfo.label}', ` + - `value: '${trackInfo.value}' attached page: ${this.currentPage}. Error is: ${err}.`, - type: FileLogMessageType.Error, - }); - - reject(err); - return; - } - - this.analyticsLoggingService.logData({ - message: `Tracked event with category: '${trackInfo.category}', action: '${trackInfo.action}', label: '${trackInfo.label}', value: '${trackInfo.value}' attached page: ${this.currentPage}.`, - }); - resolve(); - } - ); - }); + private toEventName(action: string): string { + const name = (action || "") + .replace(/[^A-Za-z0-9_]/g, "_") + .replace(/^[^A-Za-z]+/, "") + .slice(0, MAX_EVENT_NAME_LENGTH); + + return name || "cli_event"; } - private trackPageView( - visitor: ua.Visitor, - trackInfo: IGoogleAnalyticsPageviewData - ): Promise { - return new Promise((resolve, reject) => { - this.currentPage = trackInfo.path; - - const pageViewData: ua.PageviewParams = { - dp: trackInfo.path, - dt: trackInfo.title, - }; - - visitor.pageview(pageViewData, (err) => { - if (err) { - this.analyticsLoggingService.logData({ - message: `Unable to track pageview with path '${trackInfo.path}' and title: '${trackInfo.title}' Error is: ${err}.`, - type: FileLogMessageType.Error, - }); - - reject(err); - return; - } - - this.analyticsLoggingService.logData({ - message: `Tracked pageview with path '${trackInfo.path}' and title: '${trackInfo.title}'.`, - }); - resolve(); - }); - }); + private truncate(value: string): string { + return _.isNil(value) ? value : `${value}`.slice(0, MAX_PARAM_VALUE_LENGTH); } } diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 77cda49684..88098d55b0 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -58,7 +58,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private $filesHashService: IFilesHashService, public $hooksService: IHooksService, private $injector: IInjector, - private $watchIgnoreListService: IWatchIgnoreListService + private $watchIgnoreListService: IWatchIgnoreListService, ) {} private static MANIFEST_ROOT = { @@ -91,7 +91,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private async updateManifestContent( oldManifestContent: string, - defaultPackageName: string + defaultPackageName: string, ): Promise { let xml: any = await this.getXml(oldManifestContent); @@ -139,14 +139,14 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { } else { resolve(result); } - }) + }), ); return promise; } private getIncludeGradleCompileDependenciesScope( - includeGradleFileContent: string + includeGradleFileContent: string, ): Array { const indexOfDependenciesScope = includeGradleFileContent.indexOf("dependencies"); @@ -163,14 +163,14 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { if (indexOfRepositoriesScope >= 0) { repositoriesScope = this.getScope( "repositories", - includeGradleFileContent + includeGradleFileContent, ); result.push(repositoriesScope); } const dependenciesScope = this.getScope( "dependencies", - includeGradleFileContent + includeGradleFileContent, ); result.push(dependenciesScope); @@ -224,13 +224,13 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.validateOptions(options); const manifestFilePath = this.getManifest(options.platformsAndroidDirPath); const androidSourceDirectories = this.getAndroidSourceDirectories( - options.platformsAndroidDirPath + options.platformsAndroidDirPath, ); const shortPluginName = getShortPluginName(options.pluginName); const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName); const pluginSourceFileHashesInfo = await this.getSourceFilesHashes( options.platformsAndroidDirPath, - shortPluginName + shortPluginName, ); const shouldBuildAar = await this.shouldBuildAar({ @@ -249,17 +249,17 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { await this.updateManifest( manifestFilePath, pluginTempMainSrcDir, - shortPluginName + shortPluginName, ); this.copySourceSetDirectories( androidSourceDirectories, - pluginTempMainSrcDir + pluginTempMainSrcDir, ); await this.setupGradle( pluginTempDir, options.platformsAndroidDirPath, options.projectDir, - options.pluginName + options.pluginName, ); await this.buildPlugin({ gradlePath: options.gradlePath, @@ -269,7 +269,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { projectDir: options.projectDir, }); this.$watchIgnoreListService.addFileToIgnoreList( - path.join(options.aarOutputDir, `${shortPluginName}.aar`) + path.join(options.aarOutputDir, `${shortPluginName}.aar`), ); this.copyAar(shortPluginName, pluginTempDir, options.aarOutputDir); this.writePluginHashInfo(pluginSourceFileHashesInfo, pluginTempDir); @@ -286,22 +286,22 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private getSourceFilesHashes( pluginTempPlatformsAndroidDir: string, - shortPluginName: string + shortPluginName: string, ): Promise { const pathToAar = path.join( pluginTempPlatformsAndroidDir, - `${shortPluginName}.aar` + `${shortPluginName}.aar`, ); const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync( pluginTempPlatformsAndroidDir, - (file: string, stat: IFsStats) => file !== pathToAar + (file: string, stat: IFsStats) => file !== pathToAar, ); return this.$filesHashService.generateHashes(pluginNativeDataFiles); } private writePluginHashInfo( fileHashesInfo: IStringDictionary, - pluginTempDir: string + pluginTempDir: string, ): void { const buildDataFile = this.getPathToPluginBuildDataFile(pluginTempDir); this.$fs.writeJson(buildDataFile, fileHashesInfo); @@ -322,17 +322,17 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { shouldBuildAar && this.$fs.exists(opts.pluginTempDir) && this.$fs.exists( - path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`) + path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`), ) ) { const buildDataFile = this.getPathToPluginBuildDataFile( - opts.pluginTempDir + opts.pluginTempDir, ); if (this.$fs.exists(buildDataFile)) { const oldHashes = this.$fs.readJson(buildDataFile); shouldBuildAar = this.$filesHashService.hasChangesInShasums( oldHashes, - opts.fileHashesInfo + opts.fileHashesInfo, ); } } @@ -347,7 +347,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private async updateManifest( manifestFilePath: string, pluginTempMainSrcDir: string, - shortPluginName: string + shortPluginName: string, ): Promise { let updatedManifestContent; this.$fs.ensureDirectoryExists(pluginTempMainSrcDir); @@ -358,13 +358,13 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { androidManifestContent = this.$fs.readText(manifestFilePath); } catch (err) { this.$errors.fail( - `Failed to fs.readFileSync the manifest file located at ${manifestFilePath}. Error is: ${err.toString()}` + `Failed to fs.readFileSync the manifest file located at ${manifestFilePath}. Error is: ${err.toString()}`, ); } updatedManifestContent = await this.updateManifestContent( androidManifestContent, - defaultPackageName + defaultPackageName, ); } else { updatedManifestContent = this.createManifestContent(defaultPackageName); @@ -372,20 +372,20 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { const pathToTempAndroidManifest = path.join( pluginTempMainSrcDir, - MANIFEST_FILE_NAME + MANIFEST_FILE_NAME, ); try { this.$fs.writeFile(pathToTempAndroidManifest, updatedManifestContent); } catch (e) { this.$errors.fail( - `Failed to write the updated AndroidManifest in the new location - ${pathToTempAndroidManifest}. Error is: ${e.toString()}` + `Failed to write the updated AndroidManifest in the new location - ${pathToTempAndroidManifest}. Error is: ${e.toString()}`, ); } } private copySourceSetDirectories( androidSourceSetDirectories: string[], - pluginTempMainSrcDir: string + pluginTempMainSrcDir: string, ): void { for (const dir of androidSourceSetDirectories) { const dirName = path.basename(dir); @@ -400,10 +400,10 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { pluginTempDir: string, platformsAndroidDirPath: string, projectDir: string, - pluginName: string + pluginName: string, ): Promise { const gradleTemplatePath = path.resolve( - path.join(__dirname, "../../vendor/gradle-plugin") + path.join(__dirname, "../../vendor/gradle-plugin"), ); const allGradleTemplateFiles = path.join(gradleTemplatePath, "*"); const buildGradlePath = path.join(pluginTempDir, "build.gradle"); @@ -411,16 +411,15 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.$fs.copyFile(allGradleTemplateFiles, pluginTempDir); this.addCompileDependencies(platformsAndroidDirPath, buildGradlePath); - const runtimeGradleVersions = await this.getRuntimeGradleVersions( - projectDir - ); + const runtimeGradleVersions = + await this.getRuntimeGradleVersions(projectDir); this.replaceGradleVersion( pluginTempDir, - runtimeGradleVersions.gradleVersion + runtimeGradleVersions.gradleVersion, ); this.replaceGradleAndroidPluginVersion( buildGradlePath, - runtimeGradleVersions.gradleAndroidPluginVersion + runtimeGradleVersions.gradleAndroidPluginVersion, ); this.replaceFileContent(buildGradlePath, "{{pluginName}}", pluginName); this.replaceFileContent(settingsGradlePath, "{{pluginName}}", pluginName); @@ -432,7 +431,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { pluginTempDir, "src", "main", - "AndroidManifest.xml" + "AndroidManifest.xml", ); const manifestContent = this.$fs.readText(manifestPath); @@ -447,41 +446,40 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.replaceFileContent( buildGradlePath, "{{pluginNamespace}}", - packageName + packageName, ); } private async getRuntimeGradleVersions( - projectDir: string + projectDir: string, ): Promise { let runtimeGradleVersions: IRuntimeGradleVersions = null; if (projectDir) { const projectData = this.$projectDataService.getProjectData(projectDir); const platformData = this.$platformsDataService.getPlatformData( this.$devicePlatformsConstants.Android, - projectData + projectData, ); const projectRuntimeVersion = platformData.platformProjectService.getFrameworkVersion(projectData); runtimeGradleVersions = await this.getGradleVersions( - projectRuntimeVersion + projectRuntimeVersion, ); this.$logger.trace( `Got gradle versions ${JSON.stringify( - runtimeGradleVersions - )} from runtime v${projectRuntimeVersion}` + runtimeGradleVersions, + )} from runtime v${projectRuntimeVersion}`, ); } if (!runtimeGradleVersions) { const latestRuntimeVersion = await this.getLatestRuntimeVersion(); - runtimeGradleVersions = await this.getGradleVersions( - latestRuntimeVersion - ); + runtimeGradleVersions = + await this.getGradleVersions(latestRuntimeVersion); this.$logger.trace( `Got gradle versions ${JSON.stringify( - runtimeGradleVersions - )} from the latest runtime v${latestRuntimeVersion}` + runtimeGradleVersions, + )} from the latest runtime v${latestRuntimeVersion}`, ); } @@ -490,23 +488,21 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private async getLatestRuntimeVersion(): Promise { let runtimeVersion: string = null; - + const packageName = + this.$projectData.nsConfig?.android?.runtimePackageName || + SCOPED_ANDROID_RUNTIME_NAME; try { - let result = await this.$packageManager.view( - SCOPED_ANDROID_RUNTIME_NAME, - { - "dist-tags": true, - } - ); + let result = await this.$packageManager.view(packageName, { + "dist-tags": true, + }); result = result?.["dist-tags"] ?? result; runtimeVersion = result.latest; } catch (err) { this.$logger.trace( - `Error while getting latest android runtime version from view command: ${err}` - ); - const registryData = await this.$packageManager.getRegistryPackageData( - SCOPED_ANDROID_RUNTIME_NAME + `Error while getting latest android runtime version from view command: ${err}`, ); + const registryData = + await this.$packageManager.getRegistryPackageData(packageName); runtimeVersion = registryData["dist-tags"].latest; } @@ -529,12 +525,15 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { }; } + const packageName = + this.$projectData.nsConfig?.android?.runtimePackageName || + SCOPED_ANDROID_RUNTIME_NAME; // try reading from installed runtime first before reading from the npm registry... const installedRuntimePackageJSONPath = resolvePackageJSONPath( - SCOPED_ANDROID_RUNTIME_NAME, + packageName, { paths: [this.$projectData.projectDir], - } + }, ); if (!installedRuntimePackageJSONPath) { @@ -542,7 +541,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { } const installedRuntimePackageJSON: IRuntimePackageJSON = this.$fs.readJson( - installedRuntimePackageJSONPath + installedRuntimePackageJSONPath, ); if (!installedRuntimePackageJSON) { @@ -572,7 +571,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { } private async getGradleVersions( - runtimeVersion: string + runtimeVersion: string, ): Promise { let runtimeGradleVersions: { versions: { gradle: string; gradleAndroid: string }; @@ -584,11 +583,14 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { return localVersionInfo; } + const packageName = + this.$projectData.nsConfig?.android?.runtimePackageName || + SCOPED_ANDROID_RUNTIME_NAME; // fallback to reading from npm... try { let output = await this.$packageManager.view( - `${SCOPED_ANDROID_RUNTIME_NAME}@${runtimeVersion}`, - { version_info: true } + `${packageName}@${runtimeVersion}`, + { version_info: true }, ); output = output?.["version_info"] ?? output; @@ -602,8 +604,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { * */ output = await this.$packageManager.view( - `${SCOPED_ANDROID_RUNTIME_NAME}@${runtimeVersion}`, - { gradle: true } + `${packageName}@${runtimeVersion}`, + { gradle: true }, ); output = output?.["gradle"] ?? output; @@ -619,11 +621,10 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { runtimeGradleVersions = { versions: output }; } catch (err) { this.$logger.trace( - `Error while getting gradle data for android runtime from view command: ${err}` - ); - const registryData = await this.$packageManager.getRegistryPackageData( - SCOPED_ANDROID_RUNTIME_NAME + `Error while getting gradle data for android runtime from view command: ${err}`, ); + const registryData = + await this.$packageManager.getRegistryPackageData(packageName); runtimeGradleVersions = registryData.versions[runtimeVersion]; } @@ -656,19 +657,19 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { pluginTempDir, "gradle", "wrapper", - "gradle-wrapper.properties" + "gradle-wrapper.properties", ); this.replaceFileContent( gradleWrapperPropertiesPath, gradleVersionPlaceholder, - gradleVersion + gradleVersion, ); } private replaceGradleAndroidPluginVersion( buildGradlePath: string, - version: string + version: string, ): void { const gradleAndroidPluginVersionPlaceholder = "{{runtimeAndroidPluginVersion}}"; @@ -678,14 +679,14 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.replaceFileContent( buildGradlePath, gradleAndroidPluginVersionPlaceholder, - gradleAndroidPluginVersion + gradleAndroidPluginVersion, ); } private replaceFileContent( filePath: string, content: string, - replacement: string + replacement: string, ) { const fileContent = this.$fs.readText(filePath); const contentRegex = new RegExp(content, "g"); @@ -695,11 +696,11 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private addCompileDependencies( platformsAndroidDirPath: string, - buildGradlePath: string + buildGradlePath: string, ): void { const includeGradlePath = path.join( platformsAndroidDirPath, - INCLUDE_GRADLE_NAME + INCLUDE_GRADLE_NAME, ); if (this.$fs.exists(includeGradlePath)) { const includeGradleContent = this.$fs.readText(includeGradlePath); @@ -709,7 +710,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { if (compileDependencies.length) { this.$fs.appendFile( buildGradlePath, - "\n" + compileDependencies.join("\n") + "\n" + compileDependencies.join("\n"), ); } } @@ -718,7 +719,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private copyAar( shortPluginName: string, pluginTempDir: string, - aarOutputDir: string + aarOutputDir: string, ): void { const finalAarName = `${shortPluginName}-release.aar`; const pathToBuiltAar = path.join( @@ -726,7 +727,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { "build", "outputs", "aar", - finalAarName + finalAarName, ); if (this.$fs.exists(pathToBuiltAar)) { @@ -734,12 +735,12 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { if (aarOutputDir) { this.$fs.copyFile( pathToBuiltAar, - path.join(aarOutputDir, `${shortPluginName}.aar`) + path.join(aarOutputDir, `${shortPluginName}.aar`), ); } } catch (e) { this.$errors.fail( - `Failed to copy built aar to destination. ${e.message}` + `Failed to copy built aar to destination. ${e.message}`, ); } } else { @@ -756,7 +757,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { const includeGradleFilePath = path.join( options.platformsAndroidDirPath, - INCLUDE_GRADLE_NAME + INCLUDE_GRADLE_NAME, ); if (this.$fs.exists(includeGradleFilePath)) { @@ -767,30 +768,30 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { .toString(); } catch (err) { this.$errors.fail( - `Failed to fs.readFileSync the include.gradle file located at ${includeGradleFilePath}. Error is: ${err.toString()}` + `Failed to fs.readFileSync the include.gradle file located at ${includeGradleFilePath}. Error is: ${err.toString()}`, ); } const productFlavorsScope = this.getScope( "productFlavors", - includeGradleFileContent + includeGradleFileContent, ); if (productFlavorsScope) { try { const newIncludeGradleFileContent = includeGradleFileContent.replace( productFlavorsScope, - "" + "", ); this.$fs.writeFile( includeGradleFilePath, - newIncludeGradleFileContent + newIncludeGradleFileContent, ); return true; } catch (e) { this.$errors.fail( `Failed to write the updated include.gradle ` + - `in - ${includeGradleFilePath}. Error is: ${e.toString()}` + `in - ${includeGradleFilePath}. Error is: ${e.toString()}`, ); } } @@ -801,7 +802,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { @hook("buildAndroidPlugin") private async buildPlugin( - pluginBuildSettings: IBuildAndroidPluginData + pluginBuildSettings: IBuildAndroidPluginData, ): Promise { const gradlew = pluginBuildSettings.gradlePath ?? @@ -834,7 +835,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { opts.env = { USER_PROJECT_PLATFORMS_ANDROID: path.resolve( cwd(), - this.$options.hostProjectPath + this.$options.hostProjectPath, ), // TODO: couldn't `hostProjectPath` have an absolute path already? ...process.env, // TODO: any other way to pass automatically the current process.env? }; @@ -848,11 +849,11 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { gradlew, sanitizedArgs, "close", - opts + opts, ); } catch (err) { this.$errors.fail( - `Failed to build plugin ${pluginBuildSettings.pluginName} : \n${err}` + `Failed to build plugin ${pluginBuildSettings.pluginName} : \n${err}`, ); } } @@ -860,7 +861,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private validateOptions(options: IPluginBuildOptions): void { if (!options) { this.$errors.fail( - "Android plugin cannot be built without passing an 'options' object." + "Android plugin cannot be built without passing an 'options' object.", ); } @@ -870,13 +871,13 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { if (!options.aarOutputDir) { this.$logger.info( - "No aarOutputDir provided, defaulting to the build outputs directory of the plugin" + "No aarOutputDir provided, defaulting to the build outputs directory of the plugin", ); } if (!options.tempPluginDirPath) { this.$errors.fail( - "Android plugin cannot be built without passing the path to a directory where the temporary project should be built." + "Android plugin cannot be built without passing the path to a directory where the temporary project should be built.", ); } @@ -884,17 +885,17 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { } private validatePlatformsAndroidDirPathOption( - options: IPluginBuildOptions + options: IPluginBuildOptions, ): void { if (!options) { this.$errors.fail( - "Android plugin cannot be built without passing an 'options' object." + "Android plugin cannot be built without passing an 'options' object.", ); } if (!options.platformsAndroidDirPath) { this.$errors.fail( - "Android plugin cannot be built without passing the path to the platforms/android dir." + "Android plugin cannot be built without passing the path to the platforms/android dir.", ); } } diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index d2ce5ca76f..d4ba573a08 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -89,7 +89,7 @@ function topologicalSortNativeDependencies( dependencies: NativeDependency[], start: NativeDependency[] = [], depth = 0, - total = 0 // do not pass in, we calculate it in the initial run! + total = 0, // do not pass in, we calculate it in the initial run! ): NativeDependency[] { // we set the total on the initial call - and never increment it, as it's used for esacaping the recursion if (total === 0) { @@ -101,18 +101,18 @@ function topologicalSortNativeDependencies( const allSubDependenciesProcessed = currentDependency.dependencies.every( (subDependency) => { return sortedDeps.some((dep) => dep.name === subDependency); - } + }, ); if (allSubDependenciesProcessed) { sortedDeps.push(currentDependency); } return sortedDeps; }, - start + start, ); const remainingDeps = dependencies.filter( - (nativeDep) => !sortedDeps.includes(nativeDep) + (nativeDep) => !sortedDeps.includes(nativeDep), ); // recurse if we still have remaining deps @@ -122,14 +122,17 @@ function topologicalSortNativeDependencies( remainingDeps, sortedDeps, depth + 1, - total + total, ); } return sortedDeps; } -export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { +export class AndroidProjectService + extends projectServiceBaseLib.PlatformProjectServiceBase + implements IPlatformProjectService +{ private static VALUES_DIRNAME = "values"; private static VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v"; @@ -151,7 +154,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $filesHashService: IFilesHashService, private $gradleCommandService: IGradleCommandService, private $gradleBuildService: IGradleBuildService, - private $analyticsService: IAnalyticsService + private $analyticsService: IAnalyticsService, ) { super($fs, $projectDataService); } @@ -160,7 +163,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public getPlatformData(projectData: IProjectData): IPlatformData { if (!projectData && !this._platformData) { throw new Error( - "First call of getPlatformData without providing projectData." + "First call of getPlatformData without providing projectData.", ); } if (projectData && projectData.platformsDir) { @@ -168,8 +171,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject ? this.$options.hostProjectPath : path.join( projectData.platformsDir, - AndroidProjectService.ANDROID_PLATFORM_NAME - ); + AndroidProjectService.ANDROID_PLATFORM_NAME, + ); const appDestinationDirectoryArr = [ projectRoot, @@ -196,7 +199,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const packageName = this.getProjectNameFromId(projectData); const runtimePackage = this.$projectDataService.getRuntimePackage( projectData.projectDir, - constants.PlatformTypes.android + constants.PlatformTypes.android, ); this._platformData = { @@ -213,14 +216,14 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.$options.hostProjectModuleName, constants.BUILD_DIR, constants.OUTPUTS_DIR, - constants.BUNDLE_DIR + constants.BUNDLE_DIR, ); } return path.join(...deviceBuildOutputArr); }, getValidBuildOutputData: ( - buildOptions: IBuildOutputOptions + buildOptions: IBuildOutputOptions, ): IValidBuildOutputData => { const buildMode = buildOptions.release ? Configurations.Release.toLowerCase() @@ -245,7 +248,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject regexes: [ new RegExp( `(${packageName}|${this.$options.hostProjectModuleName})-.*-(${Configurations.Debug}|${Configurations.Release})(-unsigned)?${constants.APK_EXTENSION_NAME}`, - "i" + "i", ), ], }; @@ -255,7 +258,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject relativeToFrameworkConfigurationFilePath: path.join( constants.SRC_DIR, constants.MAIN_DIR, - constants.MANIFEST_FILE_NAME + constants.MANIFEST_FILE_NAME, ), fastLivesyncFileExtensions: [".jpg", ".gif", ".png", ".bmp", ".webp"], // http://developer.android.com/guide/appendix/media-formats.html }; @@ -266,12 +269,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public getCurrentPlatformVersion( platformData: IPlatformData, - projectData: IProjectData + projectData: IProjectData, ): string { const currentPlatformData: IDictionary = this.$projectDataService.getRuntimePackage( projectData.projectDir, - platformData.platformNameLowerCase + platformData.platformNameLowerCase, ); return currentPlatformData && currentPlatformData[constants.VERSION_STRING]; @@ -282,11 +285,11 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public getAppResourcesDestinationDirectoryPath( - projectData: IProjectData + projectData: IProjectData, ): string { const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated( - projectData.getAppResourcesDirectoryPath() + projectData.getAppResourcesDirectoryPath(), ); if (appResourcesDirStructureHasMigrated) { @@ -299,7 +302,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async validate( projectData: IProjectData, options: IOptions, - notConfiguredEnvOptions?: INotConfiguredEnvOptions + notConfiguredEnvOptions?: INotConfiguredEnvOptions, ): Promise { this.validatePackageName(projectData.projectIdentifiers.android); this.validateProjectName(projectData.projectName); @@ -326,21 +329,25 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async createProject( frameworkDir: string, frameworkVersion: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { + const packageName = + projectData.nsConfig.android?.runtimePackageName || + constants.SCOPED_ANDROID_RUNTIME_NAME; if ( + packageName === constants.SCOPED_ANDROID_RUNTIME_NAME && semver.lt( frameworkVersion, - AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE + AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE, ) ) { this.$errors.fail( - `The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.` + `The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`, ); } this.$fs.ensureDirectoryExists( - this.getPlatformData(projectData).projectRoot + this.getPlatformData(projectData).projectRoot, ); const androidToolsInfo = this.$androidToolsInfo.getToolsInfo({ projectDir: projectData.projectDir, @@ -353,7 +360,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.getPlatformData(projectData).projectRoot, frameworkDir, "*", - "-R" + "-R", ); // TODO: Check if we actually need this and if it should be targetSdk or compileSdk @@ -363,7 +370,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private getResDestinationDir(projectData: IProjectData): string { const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated( - projectData.getAppResourcesDirectoryPath() + projectData.getAppResourcesDirectoryPath(), ); if (appResourcesDirStructureHasMigrated) { @@ -372,7 +379,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return path.join( appResourcesDestinationPath, constants.MAIN_DIR, - constants.RESOURCES_DIR + constants.RESOURCES_DIR, ); } else { return this.getLegacyAppResourcesDestinationDirPath(projectData); @@ -381,7 +388,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private cleanResValues( targetSdkVersion: number, - projectData: IProjectData + projectData: IProjectData, ): void { const resDestinationDir = this.getResDestinationDir(projectData); const directoriesInResFolder = this.$fs.readDirectory(resDestinationDir); @@ -391,18 +398,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject dirName: dir, sdkNum: parseInt( dir.substr( - AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX.length - ) + AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX.length, + ), ), }; }) .filter( (dir) => dir.dirName.match( - AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX + AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX, ) && dir.sdkNum && - (!targetSdkVersion || targetSdkVersion < dir.sdkNum) + (!targetSdkVersion || targetSdkVersion < dir.sdkNum), ) .map((dir) => path.join(resDestinationDir, dir.dirName)); @@ -425,7 +432,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.getAppResourcesDestinationDirectoryPath(projectData); if ( this.$androidResourcesMigrationService.hasMigrated( - appResourcesDirectoryPath + appResourcesDirectoryPath, ) ) { stringsFilePath = path.join( @@ -433,13 +440,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject constants.MAIN_DIR, constants.RESOURCES_DIR, "values", - "strings.xml" + "strings.xml", ); } else { stringsFilePath = path.join( appResourcesDestinationDirectoryPath, "values", - "strings.xml" + "strings.xml", ); } @@ -448,18 +455,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject "-i", /__TITLE_ACTIVITY__/, projectData.projectName, - stringsFilePath + stringsFilePath, ); const gradleSettingsFilePath = path.join( this.getPlatformData(projectData).projectRoot, - "settings.gradle" + "settings.gradle", ); shell.sed( "-i", /__PROJECT_NAME__/, this.getProjectNameFromId(projectData), - gradleSettingsFilePath + gradleSettingsFilePath, ); try { @@ -471,12 +478,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject "-i", new RegExp(constants.PACKAGE_PLACEHOLDER_NAME), projectData.projectIdentifiers.android, - projectData.appGradlePath + projectData.appGradlePath, ); } } catch (e) { this.$logger.trace( - `Templates updated and no need for replace in app.gradle.` + `Templates updated and no need for replace in app.gradle.`, ); } } @@ -488,7 +495,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject "-i", /__PACKAGE__/, projectData.projectIdentifiers.android, - manifestPath + manifestPath, ); } @@ -516,12 +523,16 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject canUpdate: boolean, projectData: IProjectData, addPlatform?: Function, - removePlatforms?: (platforms: string[]) => Promise + removePlatforms?: (platforms: string[]) => Promise, ): Promise { + const packageName = + projectData.nsConfig.android?.runtimePackageName || + constants.SCOPED_ANDROID_RUNTIME_NAME; if ( + packageName === constants.SCOPED_ANDROID_RUNTIME_NAME && semver.eq( newVersion, - AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE + AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE, ) ) { const platformLowercase = @@ -539,18 +550,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async buildProject( projectRoot: string, projectData: IProjectData, - buildData: IAndroidBuildData + buildData: IAndroidBuildData, ): Promise { const platformData = this.getPlatformData(projectData); await this.$gradleBuildService.buildProject( platformData.projectRoot, - buildData + buildData, ); const outputPath = platformData.getBuildOutputPath(buildData); await this.$filesHashService.saveHashesForProject( this._platformData, - outputPath + outputPath, ); await this.trackKotlinUsage(projectRoot); } @@ -558,20 +569,20 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async buildForDeploy( projectRoot: string, projectData: IProjectData, - buildData?: IAndroidBuildData + buildData?: IAndroidBuildData, ): Promise { return this.buildProject(projectRoot, projectData, buildData); } public isPlatformPrepared( projectRoot: string, - projectData: IProjectData + projectData: IProjectData, ): boolean { return this.$fs.exists( path.join( this.getPlatformData(projectData).appDestinationDirectoryPath, - this.$options.hostProjectModuleName - ) + this.$options.hostProjectModuleName, + ), ); } @@ -584,12 +595,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public ensureConfigurationFileInAppResources( - projectData: IProjectData + projectData: IProjectData, ): void { const appResourcesDirectoryPath = projectData.appResourcesDirectoryPath; const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated( - appResourcesDirectoryPath + appResourcesDirectoryPath, ); let originalAndroidManifestFilePath; @@ -599,13 +610,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.$devicePlatformsConstants.Android, "src", "main", - this.getPlatformData(projectData).configurationFileName + this.getPlatformData(projectData).configurationFileName, ); } else { originalAndroidManifestFilePath = path.join( appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, - this.getPlatformData(projectData).configurationFileName + this.getPlatformData(projectData).configurationFileName, ); } @@ -613,7 +624,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject if (!manifestExists) { this.$logger.warn( - "No manifest found in " + originalAndroidManifestFilePath + "No manifest found in " + originalAndroidManifestFilePath, ); return; } @@ -621,7 +632,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject if (!appResourcesDirStructureHasMigrated) { this.$fs.copyFile( originalAndroidManifestFilePath, - this.getPlatformData(projectData).configurationFilePath + this.getPlatformData(projectData).configurationFilePath, ); } } @@ -629,7 +640,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public prepareAppResources(projectData: IProjectData): void { const platformData = this.getPlatformData(projectData); const projectAppResourcesPath = projectData.getAppResourcesDirectoryPath( - projectData.projectDir + projectData.projectDir, ); const platformsAppResourcesPath = this.getAppResourcesDestinationDirectoryPath(projectData); @@ -640,7 +651,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const appResourcesDirStructureHasMigrated = this.$androidResourcesMigrationService.hasMigrated( - projectAppResourcesPath + projectAppResourcesPath, ); if (appResourcesDirStructureHasMigrated) { this.$fs.copyFile( @@ -648,18 +659,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject projectAppResourcesPath, platformData.normalizedPlatformName, constants.SRC_DIR, - "*" + "*", ), - platformsAppResourcesPath + platformsAppResourcesPath, ); } else { this.$fs.copyFile( path.join( projectAppResourcesPath, platformData.normalizedPlatformName, - "*" + "*", ), - platformsAppResourcesPath + platformsAppResourcesPath, ); // https://github.com/NativeScript/android-runtime/issues/899 // App_Resources/Android/libs is reserved to user's aars and jars, but they should not be copied as resources @@ -676,12 +687,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async preparePluginNativeCode( pluginData: IPluginData, - projectData: IProjectData + projectData: IProjectData, ): Promise { // build Android plugins which contain AndroidManifest.xml and/or resources const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath( pluginData, - AndroidProjectService.ANDROID_PLATFORM_NAME + AndroidProjectService.ANDROID_PLATFORM_NAME, ); if (this.$fs.exists(pluginPlatformsFolderPath)) { const options: IPluginBuildOptions = { @@ -708,14 +719,14 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async removePluginNativeCode( pluginData: IPluginData, - projectData: IProjectData + projectData: IProjectData, ): Promise { // not implemented } public async beforePrepareAllPlugins( projectData: IProjectData, - dependencies?: IDependencyData[] + dependencies?: IDependencyData[], ): Promise { if (dependencies) { dependencies = this.filterUniqueDependencies(dependencies); @@ -725,41 +736,44 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async handleNativeDependenciesChange( projectData: IProjectData, - opts: IRelease + opts: IRelease, ): Promise { return; } private filterUniqueDependencies( - dependencies: IDependencyData[] + dependencies: IDependencyData[], ): IDependencyData[] { - const depsDictionary = dependencies.reduce((dict, dep) => { - const collision = dict[dep.name]; - // in case there are multiple dependencies to the same module, the one declared in the package.json takes precedence - if (!collision || collision.depth > dep.depth) { - dict[dep.name] = dep; - } - return dict; - }, >{}); + const depsDictionary = dependencies.reduce( + (dict, dep) => { + const collision = dict[dep.name]; + // in case there are multiple dependencies to the same module, the one declared in the package.json takes precedence + if (!collision || collision.depth > dep.depth) { + dict[dep.name] = dep; + } + return dict; + }, + >{}, + ); return _.values(depsDictionary); } private provideDependenciesJson( projectData: IProjectData, - dependencies: IDependencyData[] + dependencies: IDependencyData[], ): IDependencyData[] { const platformDir = this.$options.hostProjectPath ? this.$options.hostProjectPath : path.join( projectData.platformsDir, - AndroidProjectService.ANDROID_PLATFORM_NAME - ); + AndroidProjectService.ANDROID_PLATFORM_NAME, + ); const dependenciesJsonPath = path.join( platformDir, - constants.DEPENDENCIES_JSON_NAME + constants.DEPENDENCIES_JSON_NAME, ); let nativeDependencyData = dependencies.filter( - AndroidProjectService.isNativeAndroidDependency + AndroidProjectService.isNativeAndroidDependency, ); let nativeDependencies = nativeDependencyData.map( @@ -771,12 +785,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject // filter out transient dependencies that don't have native dependencies return ( nativeDependencyData.findIndex( - (nativeDep) => nativeDep.name === dep + (nativeDep) => nativeDep.name === dep, ) !== -1 ); }), } as NativeDependency; - } + }, ); nativeDependencies = topologicalSortNativeDependencies(nativeDependencies); const jsonContent = JSON.stringify(nativeDependencies, null, 4); @@ -808,7 +822,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject cwd: projectRoot, message: "Gradle stop services...", stdio: "pipe", - } + }, ); return result; @@ -822,7 +836,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async cleanDeviceTempFolder( deviceIdentifier: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier, @@ -843,7 +857,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject projectRoot: string, frameworkDir: string, files: string, - cpArg: string + cpArg: string, ): void { const paths = files.split(" ").map((p) => path.join(frameworkDir, p)); shell.cp(cpArg, paths, projectRoot); @@ -854,7 +868,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject //Enforce underscore limitation if (!/^[a-zA-Z]+(\.[a-zA-Z0-9][a-zA-Z0-9_]*)+$/.test(packageName)) { this.$errors.fail( - `Package name must look like: com.company.Name. Got: ${packageName}` + `Package name must look like: com.company.Name. Got: ${packageName}`, ); } @@ -876,7 +890,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } private getLegacyAppResourcesDestinationDirPath( - projectData: IProjectData + projectData: IProjectData, ): string { const resourcePath: string[] = [ this.$options.hostProjectModuleName, @@ -887,12 +901,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return path.join( this.getPlatformData(projectData).projectRoot, - ...resourcePath + ...resourcePath, ); } private getUpdatedAppResourcesDestinationDirPath( - projectData: IProjectData + projectData: IProjectData, ): string { const resourcePath: string[] = [ this.$options.hostProjectModuleName, @@ -901,7 +915,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return path.join( this.getPlatformData(projectData).projectRoot, - ...resourcePath + ...resourcePath, ); } @@ -921,18 +935,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private cleanUpPreparedResources(projectData: IProjectData): void { let resourcesDirPath = path.join( projectData.appResourcesDirectoryPath, - this.getPlatformData(projectData).normalizedPlatformName + this.getPlatformData(projectData).normalizedPlatformName, ); if ( this.$androidResourcesMigrationService.hasMigrated( - projectData.appResourcesDirectoryPath + projectData.appResourcesDirectoryPath, ) ) { resourcesDirPath = path.join( resourcesDirPath, constants.SRC_DIR, constants.MAIN_DIR, - constants.RESOURCES_DIR + constants.RESOURCES_DIR, ); } @@ -963,7 +977,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } catch (e) { this.$logger.trace( - `Failed to track android build statistics. Error is: ${e.message}` + `Failed to track android build statistics. Error is: ${e.message}`, ); } } @@ -972,7 +986,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const staticsFilePath = path.join( projectRoot, constants.ANDROID_ANALYTICS_DATA_DIR, - constants.ANDROID_ANALYTICS_DATA_FILE + constants.ANDROID_ANALYTICS_DATA_FILE, ); let buildStatistics; @@ -981,7 +995,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject buildStatistics = this.$fs.readJson(staticsFilePath); } catch (e) { this.$logger.trace( - `Unable to read android build statistics file. Error is ${e.message}` + `Unable to read android build statistics file. Error is ${e.message}`, ); } } diff --git a/lib/services/android/android-bundle-tool-service.ts b/lib/services/android/android-bundle-tool-service.ts index 32dba7d8e1..7afe726bf3 100644 --- a/lib/services/android/android-bundle-tool-service.ts +++ b/lib/services/android/android-bundle-tool-service.ts @@ -1,31 +1,61 @@ -import { resolve, join } from "path"; +import { join } from "path"; import * as _ from "lodash"; import { hasValidAndroidSigning } from "../../common/helpers"; -import { IChildProcess, ISysInfo, IErrors } from "../../common/declarations"; +import { + IChildProcess, + IErrors, + IFileSystem, + ISettingsService, + ISysInfo, + Server, +} from "../../common/declarations"; import { IAndroidBundleToolService, IBuildApksOptions, IInstallApksOptions, } from "../../definitions/android-bundle-tool-service"; +import { ITerminalSpinnerService } from "../../definitions/terminal-spinner-service"; +import { + BUNDLETOOL_CACHE_DIRNAME, + BUNDLETOOL_PATH_ENV_VAR, + BUNDLETOOL_RELEASES_URL, + BUNDLETOOL_SHA256, + BUNDLETOOL_VERSION, +} from "../../constants"; import { injector } from "../../common/yok"; export class AndroidBundleToolService implements IAndroidBundleToolService { + // a cold download of ~32MB can outlast the default 10s lock, and a second + // process has to keep waiting rather than pull the same jar again + private static LOCK_OPTIONS: ILockOptions = { + stale: 5 * 60 * 1000, + retriesObj: { + retries: 300, + minTimeout: 200, + maxTimeout: 2000, + factor: 1.5, + }, + }; + private javaPath: string; - private aabToolPath: string; + private bundleToolPathPromise: Promise; + constructor( private $childProcess: IChildProcess, private $sysInfo: ISysInfo, - private $errors: IErrors - ) { - this.aabToolPath = resolve( - join(__dirname, "../../../vendor/aab-tool/bundletool.jar") - ); - } + private $errors: IErrors, + private $fs: IFileSystem, + private $httpClient: Server.IHttpClient, + private $lockService: ILockService, + private $logger: ILogger, + private $settingsService: ISettingsService, + private $terminalSpinnerService: ITerminalSpinnerService, + ) {} public async buildApks(options: IBuildApksOptions): Promise { if (!hasValidAndroidSigning(options.signingData)) { this.$errors.fail( - `Unable to build "apks" without a full signing information.` + `Unable to build "apks" without a full signing information.`, ); } @@ -46,7 +76,7 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { ]); if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) { this.$errors.fail( - `Unable to build "apks" from the provided "aab". Error: ${aabToolResult.stderr}` + `Unable to build "apks" from the provided "aab". Error: ${aabToolResult.stderr}`, ); } } @@ -61,18 +91,18 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { ]); if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) { this.$errors.fail( - `Unable to install "apks" on device "${options.deviceId}". Error: ${aabToolResult.stderr}` + `Unable to install "apks" on device "${options.deviceId}". Error: ${aabToolResult.stderr}`, ); } } private async execBundleTool(args: string[]) { const javaPath = await this.getJavaPath(); - const defaultArgs = ["-jar", this.aabToolPath]; + const defaultArgs = ["-jar", await this.getBundleToolPath()]; const result = await this.$childProcess.trySpawnFromCloseEvent( javaPath, - _.concat(defaultArgs, args) + _.concat(defaultArgs, args), ); return result; @@ -85,6 +115,130 @@ export class AndroidBundleToolService implements IAndroidBundleToolService { return this.javaPath; } + + private getBundleToolPath(): Promise { + this.bundleToolPathPromise = + this.bundleToolPathPromise || this.resolveBundleToolPath(); + + return this.bundleToolPathPromise; + } + + private async resolveBundleToolPath(): Promise { + const customPath = process.env[BUNDLETOOL_PATH_ENV_VAR]; + if (customPath) { + if (!this.$fs.exists(customPath)) { + this.$errors.fail( + `${BUNDLETOOL_PATH_ENV_VAR} is set to "${customPath}", but no file exists there.`, + ); + } + + this.$logger.trace( + `Using bundletool from ${BUNDLETOOL_PATH_ENV_VAR}: ${customPath}`, + ); + return customPath; + } + + const cacheDir = join( + this.$settingsService.getProfileDir(), + BUNDLETOOL_CACHE_DIRNAME, + ); + const jarPath = join(cacheDir, `bundletool-all-${BUNDLETOOL_VERSION}.jar`); + this.$fs.ensureDirectoryExists(cacheDir); + + if (await this.isExpectedJar(jarPath)) { + return jarPath; + } + + return this.$lockService.executeActionWithLock( + async () => { + // whoever held the lock before us may have just downloaded it + if (await this.isExpectedJar(jarPath)) { + return jarPath; + } + + await this.downloadBundleTool(jarPath); + return jarPath; + }, + join(cacheDir, `bundletool-${BUNDLETOOL_VERSION}.lock`), + AndroidBundleToolService.LOCK_OPTIONS, + ); + } + + private async isExpectedJar(jarPath: string): Promise { + if (!this.$fs.exists(jarPath)) { + return false; + } + + const shasum = await this.$fs.getFileShasum(jarPath, { + algorithm: "sha256", + }); + if (shasum === BUNDLETOOL_SHA256) { + return true; + } + + this.$logger.warn( + `Cached bundletool at "${jarPath}" does not match the expected checksum and will be downloaded again.`, + ); + this.$fs.deleteFile(jarPath); + return false; + } + + private async downloadBundleTool(jarPath: string): Promise { + const jarName = `bundletool-all-${BUNDLETOOL_VERSION}.jar`; + const url = `${BUNDLETOOL_RELEASES_URL}/${BUNDLETOOL_VERSION}/${jarName}`; + const tempPath = `${jarPath}.download`; + const spinner = this.$terminalSpinnerService.createSpinner(); + + spinner.start(`Downloading bundletool ${BUNDLETOOL_VERSION}`); + + try { + await this.$httpClient.httpRequest({ + url, + method: "GET", + // identity keeps Content-Length equal to the real jar size, so the + // progress readout is not skewed by a re-compressed response + headers: { "Accept-Encoding": "identity" }, + pipeTo: this.$fs.createWriteStream(tempPath), + onDownloadProgress: (progress: { loaded: number; total?: number }) => { + spinner.text = `Downloading bundletool ${BUNDLETOOL_VERSION} ${this.formatProgress( + progress, + )}`; + }, + }); + } catch (err) { + spinner.fail(`Failed to download bundletool ${BUNDLETOOL_VERSION}`); + this.$fs.deleteFile(tempPath); + this.$errors.fail( + `Unable to download bundletool from "${url}". ` + + `Set ${BUNDLETOOL_PATH_ENV_VAR} to the path of a local bundletool jar to skip the download. ` + + `Error: ${err.message}`, + ); + } + + const shasum = await this.$fs.getFileShasum(tempPath, { + algorithm: "sha256", + }); + if (shasum !== BUNDLETOOL_SHA256) { + spinner.fail(`Failed to download bundletool ${BUNDLETOOL_VERSION}`); + this.$fs.deleteFile(tempPath); + this.$errors.fail( + `Checksum mismatch for bundletool downloaded from "${url}". ` + + `Expected ${BUNDLETOOL_SHA256}, got ${shasum}.`, + ); + } + + // rename is atomic, so a concurrent reader never sees a partial jar + this.$fs.rename(tempPath, jarPath); + spinner.succeed(`Downloaded bundletool ${BUNDLETOOL_VERSION}`); + } + + private formatProgress(progress: { loaded: number; total?: number }): string { + const toMb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1); + + return progress.total + ? `${toMb(progress.loaded)}/${toMb(progress.total)} MB` + : `${toMb(progress.loaded)} MB`; + } } injector.register("androidBundleToolService", AndroidBundleToolService); diff --git a/lib/services/android/gradle-build-args-service.ts b/lib/services/android/gradle-build-args-service.ts index 2d0ce3ddec..afe2449c62 100644 --- a/lib/services/android/gradle-build-args-service.ts +++ b/lib/services/android/gradle-build-args-service.ts @@ -85,7 +85,15 @@ export class GradleBuildArgsService implements IGradleBuildArgsService { } private getBuildTaskName(buildData: IAndroidBuildData): string { - const baseTaskName = buildData.androidBundle ? "bundle" : "assemble"; + let baseTaskName = buildData.androidBundle ? "bundle" : "assemble"; + + // a product flavor sits between the task and the build type - + // `assembleFooRelease`, `bundleFooDebug` + const flavor = buildData.gradleFlavor; + if (flavor) { + baseTaskName += flavor[0].toUpperCase() + flavor.slice(1); + } + const buildTaskName = buildData.release ? `${baseTaskName}${Configurations.Release}` : `${baseTaskName}${Configurations.Debug}`; diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index 3eb880b2ba..04e73041ea 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -1,5 +1,6 @@ import * as path from "path"; import * as child_process from "child_process"; +import * as net from "net"; import * as semver from "semver"; import * as _ from "lodash"; import { EventEmitter } from "events"; @@ -9,6 +10,7 @@ import { BUNDLER_COMPILATION_COMPLETE, PackageManagers, CONFIG_FILE_NAME_DISPLAY, + VITE_DIST_FOLDER_NAME, } from "../../constants"; import { IPackageManager, @@ -31,6 +33,7 @@ import { IHostInfo, } from "../../common/declarations"; import { ICleanupService } from "../../definitions/cleanup-service"; +import { ViteHmrPortService } from "../../contracts/vite-hmr-port-service"; import { injector } from "../../common/yok"; import { resolvePackagePath, @@ -58,6 +61,10 @@ export class BundlerCompilerService implements IBundlerCompilerService { private bundlerProcesses: IDictionary = {}; + // Vite-only: the long-lived `vite serve` dev server the device fetches + // modules and HMR updates from. Keyed by platform, managed by this CLI + // so users no longer need a separate `concurrently`/`wait-on` process. + private viteServeProcesses: IDictionary = {}; private expectedHashes: IStringDictionary = {}; constructor( @@ -73,10 +80,44 @@ export class BundlerCompilerService private $packageManager: IPackageManager, private $packageInstallationManager: IPackageInstallationManager, // private $sharedEventBus: ISharedEventBus private $projectConfigService: IProjectConfigService, + private $viteHmrPortService: ViteHmrPortService, ) { super(); } + /** + * Project-relative directory Vite stages its output in before the CLI + * copies it into the platform app. Each platform gets its own directory + * so concurrent iOS and Android sessions (separate terminals or one + * `ns run`) never overwrite each other's bundle or vendor manifest. + * `NS_VITE_DIST_DIR` overrides it verbatim. + */ + private getViteDistRelativeDir(platform: string): string { + return ( + process.env.NS_VITE_DIST_DIR || `${VITE_DIST_FOLDER_NAME}/${platform}` + ); + } + + private getViteDistOutputPath(projectDir: string, platform: string): string { + return path.join(projectDir, this.getViteDistRelativeDir(platform)); + } + + private getViteBuildPaths( + platformData: IPlatformData, + projectData: IProjectData, + ) { + return { + distOutput: this.getViteDistOutputPath( + projectData.projectDir, + platformData.platformNameLowerCase, + ), + destDir: path.join( + platformData.appDestinationDirectoryPath, + this.$options.hostProjectModuleName, + ), + }; + } + public async compileWithWatch( platformData: IPlatformData, projectData: IProjectData, @@ -90,6 +131,25 @@ export class BundlerCompilerService let isFirstBundlerWatchCompilation = true; prepareData.watch = true; + + // Bring up the Vite HMR dev server the device fetches modules / + // HMR updates from. No-op unless bundler is vite + hmr + watch. + // Fired in parallel with the build watcher; both child processes + // inherit the adb-reverse env the run-controller set before + // prepare, so neither one spawns adb on its own. Not awaited HERE + // — but the first-build resolution below gates on it, because the + // app is (re)started as soon as `compileWithWatch` resolves and its + // very first HTTP module fetch dies on connection-refused if the + // server hasn't bound yet (a cold `vite serve` config load + vendor + // prebuild competes with this build watcher for CPU and can take + // 10-30s). Running both in parallel keeps the happy-path wall time + // at max(first build, server bind) instead of their sum. + const viteDevServerStartup = this.startViteDevServer( + platformData, + projectData, + prepareData, + ); + try { const childProcess = await this.startBundleProcess( platformData, @@ -126,13 +186,9 @@ export class BundlerCompilerService } // Copy Vite output files directly to platform destination - const distOutput = path.join( - projectData.projectDir, - ".ns-vite-build", - ); - const destDir = path.join( - platformData.appDestinationDirectoryPath, - this.$options.hostProjectModuleName, + const { distOutput, destDir } = this.getViteBuildPaths( + platformData, + projectData, ); if (debugLog) { @@ -161,7 +217,12 @@ export class BundlerCompilerService this.copyViteBundleToNative(distOutput, destDir); } - // Resolve the promise on first build completion + // Resolve the promise on first build completion — gated on + // the HMR dev server being reachable (no-op resolve for + // non-HMR runs). `startViteDevServer` never rejects and its + // readiness probe is bounded, so this cannot hang the run; + // on probe timeout we proceed and the device's own retry + // handles any residual gap. if (isFirstBundlerWatchCompilation) { isFirstBundlerWatchCompilation = false; if (debugLog) { @@ -169,7 +230,8 @@ export class BundlerCompilerService "Vite first build completed, resolving compileWithWatch", ); } - resolve(childProcess); + viteDevServerStartup.then(() => resolve(childProcess)); + return; } // Transform Vite message to match webpack format @@ -372,6 +434,8 @@ export class BundlerCompilerService reject(err); }); + const isVite = this.getBundler() === "vite"; + childProcess.on("close", async (arg: any) => { await this.$cleanupService.removeKillProcess( childProcess.pid.toString(), @@ -380,7 +444,30 @@ export class BundlerCompilerService delete this.bundlerProcesses[platformData.platformNameLowerCase]; const exitCode = typeof arg === "number" ? arg : arg && arg.code; if (exitCode === 0) { - resolve(); + // Non-watch Vite builds spawn the child with stdio:"inherit" + // (no IPC channel), so the emittedFiles message handler in + // compileWithWatch never fires and the Vite output is never + // copied to the platforms app folder. Mirror that copy step + // here so release/CI prepare and build flows actually deploy + // the freshly built bundle. Without this, the deploy folder + // is left empty (or worse, runs stale dev/HMR artifacts from + // a previous `ns debug` run) and the runtime crashes on + // launch with `Check failed: has_pending_exception()`. + // The copy must succeed for the build to succeed — a build + // whose bundle never reached the native app is not a + // successful build, so copy failures reject here. + try { + if (isVite) { + const { distOutput, destDir } = this.getViteBuildPaths( + platformData, + projectData, + ); + this.copyViteBundleToNative(distOutput, destDir, null, true); + } + resolve(); + } catch (error) { + reject(error); + } } else { const error: any = new Error( `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, @@ -408,12 +495,14 @@ export class BundlerCompilerService } private async shouldUsePreserveSymlinksOption(): Promise { - // pnpm does not require symlink (https://github.com/nodejs/node-eps/issues/46#issuecomment-277373566) + // pnpm and Bun's isolated linker do not require symlink (https://github.com/nodejs/node-eps/issues/46#issuecomment-277373566) // and it also does not work in some cases. // Check https://github.com/NativeScript/nativescript-cli/issues/5259 for more information const currentPackageManager = await this.$packageManager.getPackageManagerName(); - const res = currentPackageManager !== PackageManagers.pnpm; + const res = + currentPackageManager !== PackageManagers.pnpm && + currentPackageManager !== PackageManagers.bun; return res; } @@ -492,6 +581,12 @@ export class BundlerCompilerService ...process.env, NATIVESCRIPT_WEBPACK_ENV: JSON.stringify(envData), NATIVESCRIPT_BUNDLER_ENV: JSON.stringify(envData), + ...(isVite + ? await this.getViteChildEnv( + platformData.platformNameLowerCase, + prepareData, + ) + : {}), }; if (this.$hostInfo.isWindows) { Object.assign(options.env, { APPDATA: process.env.appData }); @@ -521,6 +616,201 @@ export class BundlerCompilerService return childProcess; } + /** + * Whether this prepare runs the long-lived Vite HMR dev server (vite + + * HMR + watch, not release). + */ + private isViteHmrSession(prepareData: IPrepareData): boolean { + return ( + this.getBundler() === "vite" && + !!prepareData.watch && + !!prepareData.hmr && + !prepareData.release + ); + } + + /** + * Environment both Vite children (the build watcher and the dev server) + * must share for a platform: the staging directory, and — for HMR + * sessions — the dev-server port. The port is resolved here, once, and + * handed to `@nativescript/vite` as `NS_HMR_PORT`, so the URLs baked into + * `bundle.mjs`, the server's bind and the `adb reverse` tunnel all match. + */ + private async getViteChildEnv( + platform: string, + prepareData: IPrepareData, + ): Promise { + const env: IStringDictionary = { + NS_VITE_DIST_DIR: this.getViteDistRelativeDir(platform), + }; + if (this.isViteHmrSession(prepareData)) { + env.NS_HMR_PORT = String( + await this.$viteHmrPortService.getPort(platform), + ); + } + return env; + } + + /** + * Spawn and manage the Vite dev server (`vite serve`) for HMR. + * + * Why the CLI owns this. With Vite, HMR needs a long-lived dev server + * (HTTP + the `/ns-hmr` websocket) that the device fetches + * modules and hot updates from — it is SEPARATE from the + * `vite build --watch` process that emits the `bundle.mjs` bootstrap + * baked into the app. Historically users wired this up themselves with + * `concurrently`/`wait-on`, which left two uncoordinated processes both + * touching adb during cold start (the source of the Android + * "Searching for devices…" freeze). By spawning it here as a child of + * the CLI, the dev server inherits the CLI's environment — crucially + * `NS_ADB_REVERSE_READY`/`NS_DEVICE_SERIAL`/`NS_ADB_PATH` set by the + * run-controller — so the CLI is the single adb owner and the dev + * server never spawns adb itself. + * + * No-op unless bundler is vite, HMR is on, watch mode, and not release. + * Best-effort: failures are logged, never thrown — a dev-server hiccup + * must not fail the run. + */ + private async startViteDevServer( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise { + try { + if (!this.isViteHmrSession(prepareData)) { + return; + } + const key = platformData.platformNameLowerCase; + if (this.viteServeProcesses[key]) { + return; + } + + const viteEnv = await this.getViteChildEnv(key, prepareData); + const port = Number(viteEnv.NS_HMR_PORT); + + const envData = this.buildEnvData( + platformData.platformNameLowerCase, + projectData, + prepareData, + ); + const cliArgs = await this.buildEnvCommandLineParams( + envData, + platformData, + projectData, + prepareData, + ); + + const additionalNodeArgs = + semver.major(process.version) <= 8 ? ["--harmony"] : []; + if (await this.shouldUsePreserveSymlinksOption()) { + additionalNodeArgs.push("--preserve-symlinks"); + } + + // `vite serve` (not `build`): runs the dev server and watches on + // its own — no `--watch`. Env flags (`--env.android --env.hmr …`) + // go after `--` so vite's CLI doesn't choke on unknown options. + const args = [ + ...additionalNodeArgs, + this.getBundlerExecutablePath(projectData), + "serve", + `--config=${projectData.bundlerConfigPath}`, + `--mode=development`, + "--", + ...cliArgs, + ].filter(Boolean); + + const options: { [key: string]: any } = { + cwd: projectData.projectDir, + // Inherit so the dev server's URLs/logs stream to the user as + // before. No IPC needed here — the build watcher provides the + // bundle-complete IPC; the dev server is fetched over HTTP/ws. + stdio: "inherit", + env: { + ...process.env, + NATIVESCRIPT_BUNDLER_ENV: JSON.stringify(envData), + ...viteEnv, + }, + }; + if (this.$hostInfo.isWindows) { + Object.assign(options.env, { APPDATA: process.env.appData }); + } + + this.$logger.info( + `Starting Vite dev server (HMR) for ${key} on port ${port}…`, + ); + + const childProcess = this.$childProcess.spawn( + process.execPath, + args, + options, + ); + this.viteServeProcesses[key] = childProcess; + await this.$cleanupService.addKillProcess(childProcess.pid.toString()); + + childProcess.once("exit", (code: number) => { + delete this.viteServeProcesses[key]; + if (code) { + this.$logger.warn( + `Vite dev server for ${key} exited with code ${code}.`, + ); + } + }); + + // Bounded readiness probe so we can surface a clear log once the + // device can actually reach modules. + const ready = await this.waitForPort(port, 30000); + if (ready) { + this.$logger.info( + `Vite dev server ready on port ${port} (HMR for ${key}).`, + ); + } else { + // Warn (not trace): the first-build resolution in + // `compileWithWatch` gates on this method, so a probe timeout + // means the app will be deployed against a server that may not + // be up yet — the user should see why a boot might stall. + this.$logger.warn( + `Vite dev server port ${port} not observed open within the readiness probe window; continuing (it may bind shortly).`, + ); + } + } catch (err) { + this.$logger.warn( + `Failed to start the Vite dev server: ${err}. HMR may be unavailable.`, + ); + } + } + + /** + * Resolve true once `127.0.0.1:` accepts a TCP connection, or + * false after `timeoutMs`. Used to detect the Vite dev server is up. + */ + private waitForPort(port: number, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + return new Promise((resolve) => { + const attempt = () => { + const socket = net.connect({ port, host: "127.0.0.1" }); + let settled = false; + const done = (ok: boolean) => { + if (settled) { + return; + } + settled = true; + socket.destroy(); + if (ok) { + resolve(true); + } else if (Date.now() >= deadline) { + resolve(false); + } else { + setTimeout(attempt, 250); + } + }; + socket.once("connect", () => done(true)); + socket.once("error", () => done(false)); + socket.setTimeout(1000, () => done(false)); + }; + attempt(); + }); + } + private buildEnvData( platform: string, projectData: IProjectData, @@ -532,12 +822,14 @@ export class BundlerCompilerService const appId = projectData.projectIdentifiers[platform]; const appPath = projectData.getAppDirectoryRelativePath(); const appResourcesPath = projectData.getAppResourcesRelativeDirectoryPath(); + const buildPath = projectData.getBuildRelativeDirectoryPath(); Object.assign( envData, appId && { appId }, appPath && { appPath }, appResourcesPath && { appResourcesPath }, + buildPath && { buildPath }, { nativescriptLibPath: path.resolve( __dirname, @@ -714,6 +1006,16 @@ export class BundlerCompilerService bundlerProcess.kill("SIGINT"); delete this.bundlerProcesses[platform]; } + + // Tear down the Vite dev server we manage alongside the build watcher. + const viteServeProcess = this.viteServeProcesses[platform]; + if (viteServeProcess) { + await this.$cleanupService.removeKillProcess( + viteServeProcess.pid.toString(), + ); + viteServeProcess.kill("SIGINT"); + delete this.viteServeProcesses[platform]; + } } private handleHMRMessage( @@ -797,7 +1099,7 @@ export class BundlerCompilerService return path.resolve(packagePath, "bin", "vite.js"); } } else if (this.isModernBundler(projectData)) { - const packagePath = resolvePackagePath(`@nativescript/${bundler}`, { + const packagePath = resolvePackagePath(this.getBundlerPackageName(), { paths: [projectData.projectDir], }); @@ -806,6 +1108,19 @@ export class BundlerCompilerService } } + // Reaching here means the configured package could not be resolved. + // Falling through to plain webpack would run a bundler that does not + // understand the arguments the CLI passes, so the failure is reported + // against the package the project actually asked for. + const bundlerPackageName = this.getBundlerPackageName(); + if (bundlerPackageName !== WEBPACK_PLUGIN_NAME) { + this.$errors.fail( + `Unable to resolve '${bundlerPackageName}'. Install it in the ` + + `project, or remove the bundler configuration to use ` + + `${WEBPACK_PLUGIN_NAME}.`, + ); + } + const packagePath = resolvePackagePath("webpack", { paths: [projectData.projectDir], }); @@ -817,15 +1132,31 @@ export class BundlerCompilerService return path.resolve(packagePath, "bin", "webpack.js"); } + // Forks such as @akylas/nativescript-webpack replace the default package. + private getBundlerPackageName(): string { + const bundler = this.getBundler(); + if (bundler !== "webpack") { + return `@nativescript/${bundler}`; + } + + return this.$projectConfigService.getValue( + "webpackPackageName", + WEBPACK_PLUGIN_NAME, + ); + } + private isModernBundler(projectData: IProjectData): boolean { const bundler = this.getBundler(); switch (bundler) { case "rspack": return true; default: - const packageJSONPath = resolvePackageJSONPath(WEBPACK_PLUGIN_NAME, { - paths: [projectData.projectDir], - }); + const packageJSONPath = resolvePackageJSONPath( + this.getBundlerPackageName(), + { + paths: [projectData.projectDir], + }, + ); if (packageJSONPath) { const packageData = this.$fs.readJson(packageJSONPath); @@ -849,6 +1180,7 @@ export class BundlerCompilerService distOutput: string, destDir: string, specificFiles: string[] = null, + failOnError = false, ) { // Clean and copy Vite output to native platform folder if (debugLog) { @@ -890,6 +1222,15 @@ export class BundlerCompilerService console.log("Full build: Copying all files."); } + // Validate the source before touching the destination — cleaning + // destDir first would wipe a previously good bundle and leave an + // empty app folder behind a missing Vite output. + if (!this.$fs.exists(distOutput)) { + throw new Error( + `Vite output directory does not exist: ${distOutput}`, + ); + } + // Clean destination directory if (this.$fs.exists(destDir)) { this.$fs.deleteDirectory(destDir); @@ -897,16 +1238,15 @@ export class BundlerCompilerService this.$fs.createDirectory(destDir); // Copy all files from dist to platform destination - if (this.$fs.exists(distOutput)) { - this.copyRecursiveSync(distOutput, destDir); - } else { - this.$logger.warn( - `Vite output directory does not exist: ${distOutput}`, - ); - } + this.copyRecursiveSync(distOutput, destDir); } } catch (error) { - this.$logger.warn(`Failed to copy Vite bundle: ${error.message}`); + const copyError = + error instanceof Error ? error : new Error(String(error)); + if (failOnError) { + throw copyError; + } + this.$logger.warn(`Failed to copy Vite bundle: ${copyError.message}`); } } diff --git a/lib/services/bundler/bundler.ts b/lib/services/bundler/bundler.ts index 08e6bda859..00d6d34ab7 100644 --- a/lib/services/bundler/bundler.ts +++ b/lib/services/bundler/bundler.ts @@ -78,9 +78,11 @@ declare global { buildType?: string; } - interface IPlatformProjectService - extends NodeJS.EventEmitter, - IPlatformProjectServiceBase { + interface IPlatformProjectService< + TBuildData extends BuildData = BuildData, + TPrepareData extends PrepareData = PrepareData, + > + extends NodeJS.EventEmitter, IPlatformProjectServiceBase { getPlatformData(projectData: IProjectData): IPlatformData; validate( projectData: IProjectData, @@ -115,10 +117,10 @@ declare global { teamId?: true | string, ): Promise; - buildProject( + buildProject( projectRoot: string, projectData: IProjectData, - buildConfig: T, + buildConfig: TBuildData, ): Promise; /** @@ -127,9 +129,9 @@ declare global { * @param {any} platformSpecificData Platform specific data required for project preparation. * @returns {void} */ - prepareProject( + prepareProject( projectData: IProjectData, - prepareData: T, + prepareData: TPrepareData, ): Promise; /** @@ -153,6 +155,11 @@ declare global { options?: any, ): Promise; + shouldRepreparePlugin?( + pluginData: IPluginData, + projectData: IProjectData, + ): boolean; + /** * Removes native code of a plugin (CocoaPods, jars, libs, src). * @param {IPluginData} Plugins data describing the plugin which should be cleaned. @@ -215,9 +222,9 @@ declare global { * Check the current state of the project, and validate against the options. * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. */ - checkForChanges( + checkForChanges( changeset: IProjectChangesInfo, - prepareData: T, + prepareData: TPrepareData, projectData: IProjectData, ): Promise; diff --git a/lib/services/bundler/vite-hmr-port-service.ts b/lib/services/bundler/vite-hmr-port-service.ts new file mode 100644 index 0000000000..74d77e696e --- /dev/null +++ b/lib/services/bundler/vite-hmr-port-service.ts @@ -0,0 +1,108 @@ +import * as net from "net"; +import { IDictionary, IErrors } from "../../common/declarations"; +import { isTruthyEnvFlag } from "../../common/helpers"; +import { injector } from "../../common/yok"; +import { ViteHmrPortService as ViteHmrPortServiceContract } from "../../contracts/vite-hmr-port-service"; + +const DEFAULT_PORT = 5173; +const MAX_PORT = 65535; + +export class ViteHmrPortServiceImpl implements ViteHmrPortServiceContract { + private ports: IDictionary> = {}; + private allocated = new Set(); + // Allocation runs one platform at a time: `ns run` resolves every + // device's platform concurrently, and two probes racing on the same + // free port would both claim it. + private queue: Promise = Promise.resolve(); + + constructor( + private $errors: IErrors, + private $logger: ILogger, + ) {} + + public getPort(platform: string): Promise { + const key = platform.toLowerCase(); + if (!this.ports[key]) { + this.ports[key] = this.queue.then(() => this.allocate(key)); + this.queue = this.ports[key].catch((): void => undefined); + } + return this.ports[key]; + } + + private async allocate(platform: string): Promise { + const preferred = this.getPreferredPort(); + const strict = isTruthyEnvFlag(process.env.NS_HMR_STRICT_PORT); + + for (let port = preferred; port <= MAX_PORT; port++) { + const busy = this.allocated.has(port) || !(await this.isPortFree(port)); + if (!busy) { + this.allocated.add(port); + if (port !== preferred) { + this.$logger.info( + `Vite dev server port ${preferred} is in use; using port ${port} for ${platform} instead.`, + ); + } + return port; + } + if (strict) { + this.$errors.fail( + `Vite dev server port ${preferred} is in use and NS_HMR_STRICT_PORT is set. Free the port, or pick another one with NS_HMR_PORT.`, + ); + } + } + + return this.$errors.fail( + `Unable to find a free port for the Vite dev server (tried ${preferred}-${MAX_PORT}). Set NS_HMR_PORT to a free port.`, + ); + } + + private getPreferredPort(): number { + const fromEnv = Number(process.env.NS_HMR_PORT); + return Number.isFinite(fromEnv) && fromEnv > 0 + ? Math.floor(fromEnv) + : DEFAULT_PORT; + } + + /** + * A port is free when the wildcard bind the dev server performs would + * succeed AND nothing answers on loopback. Both checks are needed: on + * macOS a listener bound only to `127.0.0.1` does not block a `0.0.0.0` + * bind, yet loopback is exactly what the device reaches through + * `adb reverse` and the iOS Simulator, so such a port must count as busy. + */ + private async isPortFree(port: number): Promise { + if (!(await this.canBindWildcard(port))) { + return false; + } + return !(await this.isLoopbackListening(port)); + } + + private canBindWildcard(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.unref(); + // Only EADDRINUSE means "taken"; anything else (EACCES on a + // privileged port, an unsupported address family) is left for + // the dev server itself to report. + server.once("error", (err: NodeJS.ErrnoException) => + resolve(err.code !== "EADDRINUSE"), + ); + server.listen(port, "0.0.0.0", () => server.close(() => resolve(true))); + }); + } + + private isLoopbackListening(port: number): Promise { + return new Promise((resolve) => { + const socket = net.connect({ port, host: "127.0.0.1" }); + const done = (open: boolean) => { + socket.destroy(); + resolve(open); + }; + socket.once("connect", () => done(true)); + socket.once("error", () => done(false)); + socket.setTimeout(1000, () => done(false)); + }); + } +} + +injector.register("viteHmrPortService", ViteHmrPortServiceImpl); diff --git a/lib/services/cleanup-service.ts b/lib/services/cleanup-service.ts index 4255e94a60..7defe5826a 100644 --- a/lib/services/cleanup-service.ts +++ b/lib/services/cleanup-service.ts @@ -14,6 +14,10 @@ import { IJSCleanupMessage, } from "../detached-processes/cleanup-process-definitions"; import { injector } from "../common/yok"; +import { + CleanupProcessMessage, + DetachedProcessMessages, +} from "../detached-processes/detached-process-enums"; export class CleanupService implements ICleanupService { private static CLEANUP_PROCESS_START_TIMEOUT = 10 * 1000; @@ -23,7 +27,7 @@ export class CleanupService implements ICleanupService { constructor( $options: IOptions, private $staticConfig: Config.IStaticConfig, - private $childProcess: IChildProcess + private $childProcess: IChildProcess, ) { this.pathToCleanupLogFile = $options.cleanupLogFile; } @@ -31,7 +35,7 @@ export class CleanupService implements ICleanupService { public shouldDispose = true; public async addCleanupCommand( - commandInfo: ISpawnCommandInfo + commandInfo: ISpawnCommandInfo, ): Promise { const cleanupProcess = await this.getCleanupProcess(); cleanupProcess.send({ @@ -41,7 +45,7 @@ export class CleanupService implements ICleanupService { } public async removeCleanupCommand( - commandInfo: ISpawnCommandInfo + commandInfo: ISpawnCommandInfo, ): Promise { const cleanupProcess = await this.getCleanupProcess(); cleanupProcess.send({ @@ -136,7 +140,7 @@ export class CleanupService implements ICleanupService { { stdio: ["ignore", "ignore", "ignore", "ipc"], detached: true, - } + }, ); cleanupProcess.unref(); diff --git a/lib/services/device/device-install-app-service.ts b/lib/services/device/device-install-app-service.ts index 4c5d16b6f3..16bffcb65f 100644 --- a/lib/services/device/device-install-app-service.ts +++ b/lib/services/device/device-install-app-service.ts @@ -14,7 +14,7 @@ import { import { IAnalyticsService, IFileSystem } from "../../common/declarations"; import { injector } from "../../common/yok"; -export class DeviceInstallAppService { +export class DeviceInstallAppService implements IDeviceInstallAppService { constructor( private $analyticsService: IAnalyticsService, private $buildArtifactsService: IBuildArtifactsService, @@ -23,25 +23,25 @@ export class DeviceInstallAppService { private $logger: ILogger, private $mobileHelper: Mobile.IMobileHelper, private $projectDataService: IProjectDataService, - private $platformsDataService: IPlatformsDataService + private $platformsDataService: IPlatformsDataService, ) {} public async installOnDevice( device: Mobile.IDevice, buildData: IBuildData, - packageFile?: string + packageFile?: string, ): Promise { this.$logger.info( - `Installing on device ${device.deviceInfo.identifier}...` + `Installing on device ${device.deviceInfo.identifier}...`, ); const platform = device.deviceInfo.platform.toLowerCase(); const projectData = this.$projectDataService.getProjectData( - buildData.projectDir + buildData.projectDir, ); const platformData = this.$platformsDataService.getPlatformData( platform, - projectData + projectData, ); await this.$analyticsService.trackEventActionInGoogleAnalytics({ @@ -53,13 +53,13 @@ export class DeviceInstallAppService { if (!packageFile) { packageFile = await this.$buildArtifactsService.getLatestAppPackagePath( platformData, - buildData + buildData, ); } await platformData.platformProjectService.cleanDeviceTempFolder( device.deviceInfo.identifier, - projectData + projectData, ); const appIdentifier = projectData.projectIdentifiers[platform]; @@ -69,7 +69,7 @@ export class DeviceInstallAppService { await device.applicationManager.reinstallApplication( appIdentifier, packageFile, - buildData + buildData, ); await this.updateHashesOnDevice({ @@ -83,19 +83,19 @@ export class DeviceInstallAppService { await this.$buildInfoFileService.saveDeviceBuildInfo( device, projectData, - outputFilePath + outputFilePath, ); } this.$logger.info( - `Successfully installed on device with identifier '${device.deviceInfo.identifier}'.` + `Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`, ); } public async installOnDeviceIfNeeded( device: Mobile.IDevice, buildData: IBuildData, - packageFile?: string + packageFile?: string, ): Promise { const shouldInstall = await this.shouldInstall(device, buildData); if (shouldInstall) { @@ -105,31 +105,29 @@ export class DeviceInstallAppService { public async shouldInstall( device: Mobile.IDevice, - buildData: IBuildData + buildData: IBuildData, ): Promise { const projectData = this.$projectDataService.getProjectData( - buildData.projectDir + buildData.projectDir, ); const platformData = this.$platformsDataService.getPlatformData( device.deviceInfo.platform, - projectData + projectData, ); const platform = device.deviceInfo.platform; if ( !(await device.applicationManager.isApplicationInstalled( - projectData.projectIdentifiers[platform.toLowerCase()] + projectData.projectIdentifiers[platform.toLowerCase()], )) ) { return true; } - const deviceBuildInfo: IBuildInfo = await this.$buildInfoFileService.getDeviceBuildInfo( - device, - projectData - ); + const deviceBuildInfo: IBuildInfo = + await this.$buildInfoFileService.getDeviceBuildInfo(device, projectData); const localBuildInfo = this.$buildInfoFileService.getLocalBuildInfo( platformData, - { ...buildData, buildForDevice: !device.isEmulator } + { ...buildData, buildForDevice: !device.isEmulator }, ); return ( diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 8196762464..d57018efec 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -8,12 +8,12 @@ import { NODE_MODULES_FOLDER_NAME, TNS_CORE_MODULES_NAME, } from "../constants"; +import { DoctorService } from "../contracts/doctor-service"; import { doctor, constants } from "@nativescript/doctor"; import { IProjectDataService } from "../definitions/project"; import { IVersionsService, IOptions } from "../declarations"; import { IPlatformEnvironmentRequirements } from "../definitions/platform"; import { - IDoctorService, IAnalyticsService, IHostInfo, IChildProcess, @@ -27,13 +27,13 @@ import { injector } from "../common/yok"; import { color } from "../color"; import { ITerminalSpinnerService } from "../definitions/terminal-spinner-service"; -export class DoctorService implements IDoctorService { +export class DoctorServiceImpl implements DoctorService { private static DarwinSetupScriptLocation = path.join( __dirname, "..", "..", "setup", - "mac-startup-shell-script.sh" + "mac-startup-shell-script.sh", ); private static WindowsSetupScriptExecutable = "powershell.exe"; private static WindowsSetupScriptArguments = [ @@ -50,7 +50,7 @@ export class DoctorService implements IDoctorService { private get jsonFileSettingsPath(): string { return path.join( this.$settingsService.getProfileDir(), - "doctor-cache.json" + "doctor-cache.json", ); } @@ -58,7 +58,7 @@ export class DoctorService implements IDoctorService { private get $jsonFileSettingsService(): IJsonFileSettingsService { return this.$injector.resolve( "jsonFileSettingsService", - { jsonFileSettingsPath: this.jsonFileSettingsPath } + { jsonFileSettingsPath: this.jsonFileSettingsPath }, ); } @@ -72,11 +72,11 @@ export class DoctorService implements IDoctorService { private $fs: IFileSystem, private $terminalSpinnerService: ITerminalSpinnerService, private $versionsService: IVersionsService, - private $settingsService: ISettingsService + private $settingsService: ISettingsService, ) {} public async printWarnings(configOptions?: { - trackResult: boolean; + trackResult?: boolean; projectDir?: string; runtimeVersion?: string; options?: IOptions; @@ -96,17 +96,17 @@ export class DoctorService implements IDoctorService { text: `Getting environment information ${EOL}`, }, () => - this.getInfos({ forceCheck: configOptions.forceCheck }, getInfosData) + this.getInfos({ forceCheck: configOptions.forceCheck }, getInfosData), ); const warnings = infos.filter( - (info) => info.type === constants.WARNING_TYPE_NAME + (info) => info.type === constants.WARNING_TYPE_NAME, ); const hasWarnings = warnings.length > 0; const hasAndroidWarnings = warnings.filter((warning) => - _.includes(warning.platforms, constants.ANDROID_PLATFORM_NAME) + _.includes(warning.platforms, constants.ANDROID_PLATFORM_NAME), ).length > 0; if (hasAndroidWarnings) { this.printPackageManagerTip(); @@ -126,18 +126,18 @@ export class DoctorService implements IDoctorService { this.$logger.info(color.bold("No issues were detected.")); await this.$jsonFileSettingsService.saveSetting( this.getKeyForConfiguration(getInfosData), - infos + infos, ); this.printInfosCore(infos); } try { await this.$versionsService.printVersionsInformation( - configOptions.platform + configOptions.platform, ); } catch (err) { this.$logger.error( - "Cannot get the latest versions information from npm. Please try again later." + "Cannot get the latest versions information from npm. Please try again later.", ); } @@ -146,7 +146,7 @@ export class DoctorService implements IDoctorService { await this.$injector .resolve( - "platformEnvironmentRequirements" + "platformEnvironmentRequirements", ) .checkEnvironmentRequirements({ platform: configOptions.platform, @@ -171,20 +171,21 @@ export class DoctorService implements IDoctorService { } this.$logger.info( - "Running the setup script to try and automatically configure your environment." + "Running the setup script to try and automatically configure your environment.", ); + let result: ISpawnResult; if (this.$hostInfo.isDarwin) { - await this.runSetupScriptCore( - DoctorService.DarwinSetupScriptLocation, - [] + result = await this.runSetupScriptCore( + DoctorServiceImpl.DarwinSetupScriptLocation, + [], ); } if (this.$hostInfo.isWindows) { - await this.runSetupScriptCore( - DoctorService.WindowsSetupScriptExecutable, - DoctorService.WindowsSetupScriptArguments + result = await this.runSetupScriptCore( + DoctorServiceImpl.WindowsSetupScriptExecutable, + DoctorServiceImpl.WindowsSetupScriptArguments, ); } @@ -192,6 +193,8 @@ export class DoctorService implements IDoctorService { action: TrackActionNames.RunSetupScript, additionalData: "Finished", }); + + return result; } public async canExecuteLocalBuild(configuration?: { @@ -200,6 +203,7 @@ export class DoctorService implements IDoctorService { runtimeVersion?: string; forceCheck?: boolean; }): Promise { + configuration = configuration || {}; await this.$analyticsService.trackEventActionInGoogleAnalytics({ action: TrackActionNames.CheckLocalBuildSetup, additionalData: "Starting", @@ -211,7 +215,7 @@ export class DoctorService implements IDoctorService { }; const infos = await this.getInfos( { forceCheck: configuration && configuration.forceCheck }, - sysInfoConfig + sysInfoConfig, ); const warnings = this.filterInfosByType(infos, constants.WARNING_TYPE_NAME); const hasWarnings = warnings.length > 0; @@ -228,7 +232,7 @@ export class DoctorService implements IDoctorService { infos.map((info) => this.$logger.trace(info.message)); await this.$jsonFileSettingsService.saveSetting( this.getKeyForConfiguration(sysInfoConfig), - infos + infos, ); } @@ -243,27 +247,26 @@ export class DoctorService implements IDoctorService { public checkForDeprecatedShortImportsInAppDir(projectDir: string): void { if (projectDir) { try { - const files = this.$projectDataService.getAppExecutableFiles( - projectDir - ); + const files = + this.$projectDataService.getAppExecutableFiles(projectDir); const shortImports = this.getDeprecatedShortImportsInFiles( files, - projectDir + projectDir, ); if (shortImports.length) { this.$logger.printMarkdown( - "Detected short imports in your application. Please note that `short imports are deprecated` since NativeScript 5.2.0. More information can be found in this blogpost https://www.nativescript.org/blog/say-goodbye-to-short-imports-in-nativescript" + "Detected short imports in your application. Please note that `short imports are deprecated` since NativeScript 5.2.0. More information can be found in this blogpost https://www.nativescript.org/blog/say-goodbye-to-short-imports-in-nativescript", ); shortImports.forEach((shortImport) => { this.$logger.printMarkdown( - `In file \`${shortImport.file}\` line \`${shortImport.line}\` is short import. Add \`tns-core-modules/\` in front of the required/imported module.` + `In file \`${shortImport.file}\` line \`${shortImport.line}\` is short import. Add \`tns-core-modules/\` in front of the required/imported module.`, ); }); } } catch (err) { this.$logger.trace( `Unable to validate if project has short imports. Error is`, - err + err, ); } } @@ -271,7 +274,7 @@ export class DoctorService implements IDoctorService { protected getDeprecatedShortImportsInFiles( files: string[], - projectDir: string + projectDir: string, ): { file: string; line: string }[] { const shortImportRegExp = this.getShortImportRegExp(projectDir); const shortImports: { file: string; line: string }[] = []; @@ -280,13 +283,13 @@ export class DoctorService implements IDoctorService { const fileContent = this.$fs.readText(file); const strippedComments = helpers.stripComments(fileContent); const linesToCheck = _.flatten( - strippedComments.split(/\r?\n/).map((line) => line.split(";")) + strippedComments.split(/\r?\n/).map((line) => line.split(";")), ); const linesWithRequireStatements = linesToCheck.filter( (line) => /\btns-core-modules\b/.exec(line) === null && - (/\bimport\b/.exec(line) || /\brequire\b/.exec(line)) + (/\bimport\b/.exec(line) || /\brequire\b/.exec(line)), ); for (const line of linesWithRequireStatements) { @@ -305,14 +308,14 @@ export class DoctorService implements IDoctorService { const pathToTnsCoreModules = path.join( projectDir, NODE_MODULES_FOLDER_NAME, - TNS_CORE_MODULES_NAME + TNS_CORE_MODULES_NAME, ); const coreModulesSubDirs = this.$fs .readDirectory(pathToTnsCoreModules) .filter((entry) => this.$fs .getFsStats(path.join(pathToTnsCoreModules, entry)) - .isDirectory() + .isDirectory(), ); const stringRegularExpressionsPerDir = coreModulesSubDirs.map((c) => { @@ -332,13 +335,13 @@ export class DoctorService implements IDoctorService { private async runSetupScriptCore( executablePath: string, - setupScriptArgs: string[] + setupScriptArgs: string[], ): Promise { return this.$childProcess.spawnFromEvent( executablePath, setupScriptArgs, "close", - { stdio: "inherit" } + { stdio: "inherit" }, ); } @@ -346,12 +349,12 @@ export class DoctorService implements IDoctorService { if (this.$hostInfo.isWindows) { this.$logger.info( "TIP: To avoid setting up the necessary environment variables, you can use the chocolatey package manager to install the Android SDK and its dependencies." + - EOL + EOL, ); } else if (this.$hostInfo.isDarwin) { this.$logger.info( "TIP: To avoid setting up the necessary environment variables, you can use the Homebrew package manager to install the Android SDK and its dependencies." + - EOL + EOL, ); } } @@ -390,20 +393,20 @@ export class DoctorService implements IDoctorService { private filterInfosByType( infos: NativeScriptDoctor.IInfo[], - type: string + type: string, ): NativeScriptDoctor.IInfo[] { return infos.filter((info) => info.type === type); } private getKeyForConfiguration( - sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig + sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig, ): string { const nativeScriptData = sysInfoConfig && sysInfoConfig.projectDir && JSON.stringify( this.$fs.readJson(path.join(sysInfoConfig.projectDir, "package.json")) - .nativescript + .nativescript, ); const delimiter = "__"; const key = [ @@ -426,7 +429,7 @@ export class DoctorService implements IDoctorService { private async getInfos( cacheConfig: { forceCheck: boolean }, - sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig + sysInfoConfig?: NativeScriptDoctor.ISysInfoConfig, ): Promise { const key = this.getKeyForConfiguration(sysInfoConfig); @@ -434,13 +437,13 @@ export class DoctorService implements IDoctorService { ? null : await this.$jsonFileSettingsService.getSettingValue< NativeScriptDoctor.IInfo[] - >(key); + >(key); this.$logger.trace( `getInfos cacheConfig options:`, cacheConfig, " current info from cache: ", - infosFromCache + infosFromCache, ); const infos = infosFromCache || (await doctor.getInfos(sysInfoConfig)); @@ -448,4 +451,4 @@ export class DoctorService implements IDoctorService { return infos; } } -injector.register("doctorService", DoctorService); +injector.register("doctorService", DoctorServiceImpl); diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index eff1fb0f0a..fe02c8d3ff 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -3,8 +3,10 @@ import * as _ from "lodash"; import { cache } from "../common/decorators"; import * as constants from "../constants"; import { createRegExp, regExpEscape } from "../common/helpers"; +import { reportDeprecation } from "../common/deprecation"; import { INodePackageManager, INpmsSingleResultData } from "../declarations"; import { + IDictionary, IFileSystem, ISettingsService, IStringDictionary, @@ -17,10 +19,87 @@ import { IGetExtensionCommandInfoParams, } from "../common/definitions/extensibility"; import { injector } from "../common/yok"; +import { IInjector } from "../common/definitions/yok"; +import { CommandsDelimiters } from "../common/constants"; +import { inject } from "../common/di/inject"; +import { CommandRegistry } from "../common/contracts"; +import type { DeferredCommandRejection } from "../common/contracts"; +import { DefinedCommand, isCommandDefinition } from "../common/define-command"; +import { registerDefinitionAs } from "../common/services/command-definition-adapter"; + +function isNonEmptyString(value: any): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function isCommandsMap(commands: any): boolean { + return !!commands && typeof commands === "object" && !Array.isArray(commands); +} + +/** + * A manifest entry is either the module path or an envelope carrying it under + * `path`. Unknown envelope keys are ignored on purpose: a CLI released today + * must keep loading manifests that grow new keys tomorrow. + */ +function getEntryModulePath(value: any): string { + if (isNonEmptyString(value)) { + return value; + } + + if ( + value && + typeof value === "object" && + !Array.isArray(value) && + isNonEmptyString(value.path) + ) { + return value.path; + } + + return null; +} + +const isDefaultCommandName = (name: string): boolean => + name.indexOf(CommandsDelimiters.DefaultHierarchicalCommand) !== -1; + +function describeRejection(rejection: DeferredCommandRejection): string { + switch (rejection.reason) { + case "invalid-name": + return rejection.detail; + case "claimed": + return `it is already registered by extension ${rejection.owner}`; + case "built-in": + return "it is already provided by the CLI"; + case "subcommand-parent": + return "it is already in use as the parent of its subcommands"; + case "parent-is-command": + return `'${rejection.parent}' is already registered as a command of its own, so the subcommand could never be reached`; + } +} + +/** + * Reads the names of the commands an extension contributes out of either shape + * of `nativescript.commands` - the legacy array of names, or the map of name to + * module path. + */ +function getDeclaredCommandNames( + commands: any, + opts?: { copy: boolean }, +): string[] { + if (Array.isArray(commands)) { + return opts && opts.copy ? commands.slice() : commands; + } + + if (isCommandsMap(commands)) { + return _.keys(commands); + } + + return null; +} export class ExtensibilityService implements IExtensibilityService { private customPathToExtensions: string = null; + private commandRegistry = inject(CommandRegistry); + private get pathToPackageJson(): string { return path.join(this.pathToExtensions, constants.PACKAGE_JSON_FILE_NAME); } @@ -42,6 +121,7 @@ export class ExtensibilityService implements IExtensibilityService { private $packageManager: INodePackageManager, private $settingsService: ISettingsService, private $requireService: IRequireService, + private $injector: IInjector, ) {} public async installExtension( @@ -131,12 +211,23 @@ export class ExtensibilityService implements IExtensibilityService { packageJsonData.nativescript && packageJsonData.nativescript.docs && path.join(pathToExtension, packageJsonData.nativescript.docs); - return { + const result: IExtensionData = { extensionName: packageJsonData.name, version: packageJsonData.version, docs, pathToExtension, }; + + const commands = getDeclaredCommandNames( + packageJsonData && + packageJsonData.nativescript && + packageJsonData.nativescript.commands, + ); + if (commands) { + result.commands = commands; + } + + return result; } public async loadExtension(extensionName: string): Promise { @@ -144,7 +235,23 @@ export class ExtensibilityService implements IExtensibilityService { await this.assertExtensionIsInstalled(extensionName); const pathToExtension = this.getPathToExtension(extensionName); - this.$requireService.require(pathToExtension); + const commandsMap = this.getDeclaredCommandsMap(extensionName); + + if (commandsMap) { + this.registerDeclaredCommands( + extensionName, + pathToExtension, + commandsMap, + ); + } else { + reportDeprecation({ + api: "extensions.require-time-registration", + detail: extensionName, + logger: this.$logger, + }); + this.$requireService.require(pathToExtension); + } + return this.getInstalledExtensionData(extensionName); } catch (error) { this.$logger.warn( @@ -194,10 +301,13 @@ export class ExtensibilityService implements IExtensibilityService { await this.$packageManager.getRegistryPackageData(extensionName); const latestPackageData = registryData.versions[registryData["dist-tags"].latest]; - const commands: string[] = + const commands = getDeclaredCommandNames( latestPackageData && - latestPackageData.nativescript && - latestPackageData.nativescript.commands; + latestPackageData.nativescript && + latestPackageData.nativescript.commands, + // The |* synthesis below pushes into this array. + { copy: true }, + ); if (commands && commands.length) { // For each default command we need to add its short syntax in the array of commands. // For example in case there's a default command called devices list, the commands array will contain devices|*list. @@ -243,6 +353,131 @@ export class ExtensibilityService implements IExtensibilityService { return null; } + /** + * Returns the `nativescript.commands` value of an extension only when it is a + * map of command name to module. Any other shape (the legacy array of + * command names, a missing key, an unreadable package.json) yields null and + * keeps the extension on the eager require path. + */ + private getDeclaredCommandsMap(extensionName: string): IDictionary { + let commands: any; + + try { + const packageJsonData = this.getExtensionPackageJsonData(extensionName); + commands = + packageJsonData && + packageJsonData.nativescript && + packageJsonData.nativescript.commands; + } catch (err) { + this.$logger.trace( + `Unable to read the package.json of extension ${extensionName}. Error is: ${err}`, + ); + return null; + } + + return isCommandsMap(commands) ? commands : null; + } + + /** + * Registers each declared command as a deferred load of its own module, so + * nothing from the extension is loaded until one of its commands is executed. + * A module may either register itself on load (a legacy-style + * `$injector.registerCommand(, )` at the top level) or export a + * `defineCommand` definition, which the deferred loader adapts and registers + * under the manifest key. + */ + private registerDeclaredCommands( + extensionName: string, + pathToExtension: string, + commands: IDictionary, + ): void { + // Manifest key order carries no meaning, so a parent's default command is + // registered before its siblings rather than wherever the author put it. + const commandNames = _.sortBy(_.keys(commands), (commandName) => + isDefaultCommandName(commandName) ? 0 : 1, + ); + + for (const commandName of commandNames) { + const modulePath = getEntryModulePath(commands[commandName]); + + if (!isNonEmptyString(commandName) || !modulePath) { + this.$logger.warn( + `Extension ${extensionName} declares an invalid command in its nativescript.commands: '${commandName}': ${JSON.stringify( + commands[commandName], + )}. The command name must be a non-empty string and its value either the path to its module or an object with a non-empty 'path'. Skipping this command.`, + ); + continue; + } + + const absoluteModulePath = path.join(pathToExtension, modulePath); + const result = this.commandRegistry.registerDeferredCommand(commandName, { + owner: extensionName, + source: absoluteModulePath, + load: () => + this.loadDeclaredCommand( + extensionName, + commandName, + absoluteModulePath, + ), + }); + + if (!result.registered) { + this.$logger.warn( + `Extension ${extensionName} is unable to register command '${commandName}': ${describeRejection( + result.rejection, + )}.`, + ); + } + } + } + + /** + * Runs on the first resolution of one declared command. Definition modules + * are registered here rather than by the module itself, which is what lets + * the manifest key stay authoritative for routing. + */ + private loadDeclaredCommand( + extensionName: string, + commandName: string, + absoluteModulePath: string, + ): void { + const exported = require(absoluteModulePath); + const candidate = (exported && exported.default) ?? exported; + + if (!isCommandDefinition(candidate)) { + return; + } + + this.warnOnDeclaredNameMismatch( + extensionName, + commandName, + absoluteModulePath, + candidate, + ); + registerDefinitionAs(commandName, candidate, this.$injector); + } + + private warnOnDeclaredNameMismatch( + extensionName: string, + commandName: string, + absoluteModulePath: string, + definition: DefinedCommand, + ): void { + const declaredNames = Array.isArray(definition.name) + ? definition.name + : [definition.name]; + + if (_.includes(declaredNames, commandName)) { + return; + } + + this.$logger.warn( + `Extension ${extensionName} declares command '${commandName}' in its package.json, but the definition in ${absoluteModulePath} names itself '${declaredNames.join( + "', '", + )}'. The command runs as '${commandName}' - the manifest decides how it is invoked.`, + ); + } + private getPathToExtension(extensionName: string): string { return path.join( this.pathToExtensions, diff --git a/lib/services/initialize-service.ts b/lib/services/initialize-service.ts index 8214b43a3d..a1990cd979 100644 --- a/lib/services/initialize-service.ts +++ b/lib/services/initialize-service.ts @@ -10,6 +10,7 @@ import { import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import { IExtensibilityService } from "../common/definitions/extensibility"; +import { SystemWarningsSeverity } from "../definitions/system-warnings"; export class InitializeService implements IInitializeService { // NOTE: Do not inject anything here, use $injector.resolve in the code @@ -30,17 +31,15 @@ export class InitializeService implements IInitializeService { } if (initOpts.settingsServiceOptions) { - const $settingsService = this.$injector.resolve( - "settingsService" - ); + const $settingsService = + this.$injector.resolve("settingsService"); $settingsService.setSettings(initOpts.settingsServiceOptions); } if (initOpts.extensibilityOptions) { if (initOpts.extensibilityOptions.pathToExtensions) { - const $extensibilityService = this.$injector.resolve< - IExtensibilityService - >("extensibilityService"); + const $extensibilityService = + this.$injector.resolve("extensibilityService"); $extensibilityService.pathToExtensions = initOpts.extensibilityOptions.pathToExtensions; } diff --git a/lib/services/ios-entitlements-service.ts b/lib/services/ios-entitlements-service.ts index 932fe88ab0..3e61acf855 100644 --- a/lib/services/ios-entitlements-service.ts +++ b/lib/services/ios-entitlements-service.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { PlistSession } from "plist-merge-patch"; +import { PlistSession } from "../tools/plist-merge/plist-session"; import { IPluginsService, IPluginData } from "../definitions/plugins"; import { IProjectData } from "../definitions/project"; import { IFileSystem } from "../common/declarations"; @@ -84,7 +84,7 @@ export class IOSEntitlementsService { makePatch(appEntitlementsPath); } - if ((session).patches && (session).patches.length > 0) { + if (session.hasPatches) { const plistContent = session.build(); this.$logger.trace( "App.entitlements: Write to: " + diff --git a/lib/services/ios-native-target-service.ts b/lib/services/ios-native-target-service.ts index f003329522..7f961f554a 100644 --- a/lib/services/ios-native-target-service.ts +++ b/lib/services/ios-native-target-service.ts @@ -4,8 +4,8 @@ import { IIOSNativeTargetService, IProjectData, IXcodeTargetBuildConfigurationProperty, - BuildNames, } from "../definitions/project"; +import { BuildNames } from "../constants"; import { IPlatformData } from "../definitions/platform"; import { IFileSystem } from "../common/declarations"; import { injector } from "../common/yok"; @@ -14,7 +14,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { constructor( protected $fs: IFileSystem, protected $pbxprojDomXcode: IPbxprojDomXcode, - protected $logger: ILogger + protected $logger: ILogger, ) {} public addTargetToProject( @@ -23,12 +23,12 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { targetType: string, project: IXcode.project, platformData: IPlatformData, - parentTarget?: string + parentTarget?: string, ): IXcode.target { const targetPath = path.join(targetRootPath, targetFolder); const targetRelativePath = path.relative( platformData.projectRoot, - targetPath + targetPath, ); const files = this.$fs .readDirectory(targetPath) @@ -38,20 +38,20 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { targetFolder, targetType, targetRelativePath, - parentTarget + parentTarget, ); project.addBuildPhase([], "PBXSourcesBuildPhase", "Sources", target.uuid); project.addBuildPhase( [], "PBXResourcesBuildPhase", "Resources", - target.uuid + target.uuid, ); project.addBuildPhase( [], "PBXFrameworksBuildPhase", "Frameworks", - target.uuid + target.uuid, ); project.addPbxGroup(files, targetFolder, targetPath, null, { @@ -61,7 +61,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { }); project.addToHeaderSearchPaths( targetPath, - target.pbxNativeTarget.productName + target.pbxNativeTarget.productName, ); return target; } @@ -69,7 +69,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { public prepareSigning( targetUuids: string[], projectData: IProjectData, - projectPath: string + projectPath: string, ): void { const xcode = this.$pbxprojDomXcode.Xcode.open(projectPath); const signing = xcode.getSigning(projectData.projectName); @@ -82,7 +82,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { const signingConfiguration = signing.configurations[config]; xcode.setManualSigningStyleByTargetKey( targetUuid, - signingConfiguration + signingConfiguration, ); break; } @@ -104,7 +104,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { public setXcodeTargetBuildConfigurationProperties( properties: IXcodeTargetBuildConfigurationProperty[], targetName: string, - project: IXcode.project + project: IXcode.project, ): void { properties.forEach((property) => { const buildNames = property.buildNames || [ @@ -116,7 +116,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { property.name, property.value, buildName, - targetName + targetName, ); }); }); @@ -126,7 +126,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { jsonPath: string, targetUuid: string, targetName: string, - project: IXcode.project + project: IXcode.project, ): void { if (this.$fs.exists(jsonPath)) { const configurationJson = this.$fs.readJson(jsonPath) || {}; @@ -139,7 +139,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { project.addToBuildSettings( "ASSETCATALOG_COMPILER_APPICON_NAME", configurationJson.assetcatalogCompilerAppiconName, - targetUuid + targetUuid, ); } const properties: IXcodeTargetBuildConfigurationProperty[] = []; @@ -148,7 +148,7 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { if (configurationJson.targetBuildConfigurationProperties) { _.forEach( configurationJson.targetBuildConfigurationProperties, - (value, name: string) => properties.push({ value, name }) + (value, name: string) => properties.push({ value, name }), ); } @@ -169,23 +169,23 @@ export class IOSNativeTargetService implements IIOSNativeTargetService { default: { this.$logger.warn( "Ignoring targetNamedBuildConfigurationProperties: %s. Only 'release', 'debug' are allowed.", - name + name, ); } } if (buildName) { _.forEach(value, (value, name: string) => - properties.push({ value, name, buildNames: [buildName] }) + properties.push({ value, name, buildNames: [buildName] }), ); } - } + }, ); } this.setXcodeTargetBuildConfigurationProperties( properties, targetName, - project + project, ); } } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 692a4567da..7ac6f30c0e 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -6,7 +6,7 @@ import { Configurations } from "../common/constants"; import * as helpers from "../common/helpers"; import { attachAwaitDetach } from "../common/helpers"; import * as projectServiceBaseLib from "./platform-project-service-base"; -import { PlistSession, Reporter } from "plist-merge-patch"; +import { PlistSession, Reporter } from "../tools/plist-merge/plist-session"; import { EOL } from "os"; import * as plist from "plist"; import * as fastGlob from "fast-glob"; @@ -92,9 +92,15 @@ const getConfigurationName = (release: boolean): string => { return release ? Configurations.Release : Configurations.Debug; }; -export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { +export class IOSProjectService + extends projectServiceBaseLib.PlatformProjectServiceBase + implements IPlatformProjectService +{ private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; private static IOS_PLATFORM_NAME = "ios"; + // CLI-managed folder under the platform root where we write generated + // artifacts (e.g. plugin modulemaps) so we never write into node_modules + private static GENERATED_PLUGINS_DIR_NAME = ".plugins"; constructor( $fs: IFileSystem, @@ -410,10 +416,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return undefined; } - public async cleanProject( - projectRoot: string, - projectData: IProjectData, - ): Promise { + public async cleanProject(projectRoot: string): Promise { return null; } @@ -435,6 +438,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ): Promise { const platformData = this.getPlatformData(projectData); + // On a first build, the runtime (and any other Swift packages) download + // here. Pre-resolve under a clear spinner so the subsequent + // "Xcode build..." step doesn't appear to hang while that happens. + await this.$spmService.ensureSPMDependenciesResolved( + platformData, + projectData, + ); + const handler = (data: any) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; @@ -500,12 +511,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async isDynamicFramework(frameworkPath: string): Promise { - const isDynamicFrameworkBundle = async ( - bundlePath: string, - frameworkName: string, - ) => { - const frameworkBinaryPath = path.join(bundlePath, frameworkName); - + const isDynamicFrameworkBundle = async (frameworkBinaryPath: string) => { const fileResult = ( await this.$childProcess.spawnFromEvent( "file", @@ -533,10 +539,18 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ singlePlatformFramework, path.extname(singlePlatformFramework), ); - isDynamic = await isDynamicFrameworkBundle( + let frameworkBinaryPath = path.join( singlePlatformFramework, frameworkName, ); + if (library.BinaryPath) { + frameworkBinaryPath = path.join( + frameworkPath, + library.LibraryIdentifier, + library.BinaryPath, + ); + } + isDynamic = await isDynamicFrameworkBundle(frameworkBinaryPath); break; } } @@ -546,7 +560,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ frameworkPath, path.extname(frameworkPath), ); - return await isDynamicFrameworkBundle(frameworkPath, frameworkName); + return await isDynamicFrameworkBundle( + path.join(frameworkPath, frameworkName), + ); } } @@ -656,7 +672,29 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ); project.addToHeaderSearchPaths({ relativePath: relativeHeaderSearchPath }); - this.generateModulemap(headersSubpath, libraryName); + // Write the generated modulemap into a CLI-managed folder under the + // platform root (never into node_modules). The modulemap references the + // plugin's headers in-place via relative paths, so nothing is copied. + const modulemapDir = path.join( + this.getPlatformData(projectData).projectRoot, + IOSProjectService.GENERATED_PLUGINS_DIR_NAME, + libraryName, + ); + const hasModulemap = this.generateModulemap( + headersSubpath, + libraryName, + modulemapDir, + ); + if (hasModulemap) { + // Put the modulemap dir on the header search path so clang discovers + // the module there instead of inside node_modules. + project.addToHeaderSearchPaths({ + relativePath: this.getLibSubpathRelativeToProjectPath( + modulemapDir, + projectData, + ), + }); + } this.savePbxProj(project, projectData); } @@ -875,7 +913,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } } - this.$iOSWatchAppService.removeWatchApp({ pbxProjPath }); const addedWatchApp = await this.$iOSWatchAppService.addWatchAppFromPath({ watchAppFolderPath: path.join( resourcesDirectoryPath, @@ -1164,6 +1201,52 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ); } + public shouldRepreparePlugin( + pluginData: IPluginData, + projectData: IProjectData, + ): boolean { + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath( + IOSProjectService.IOS_PLATFORM_NAME, + ); + + for (const fileName of this.getAllLibsForPluginWithFileExtension( + pluginData, + ".a", + )) { + const staticLibPath = path.join(pluginPlatformsFolderPath, fileName); + const libraryName = path.basename(staticLibPath, ".a"); + const headersSubpath = path.join( + path.dirname(staticLibPath), + "include", + libraryName, + ); + + if (!this.$fs.exists(headersSubpath)) { + continue; + } + + const headerFiles = this.$fs + .readDirectory(headersSubpath) + .filter( + (f) => + path.extname(f) === ".h" && + this.$fs.getFsStats(path.join(headersSubpath, f)).isFile(), + ); + + if ( + headerFiles.length > 0 && + !this.$fs.exists(path.join(headersSubpath, "module.modulemap")) + ) { + this.$logger.trace( + `Plugin ${pluginData.name}: modulemap missing at ${headersSubpath}, will re-prepare`, + ); + return true; + } + } + + return false; + } + public async removePluginNativeCode( pluginData: IPluginData, projectData: IProjectData, @@ -1244,13 +1327,43 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ constants.CONFIG_FILE_NAME_TS, ); if (this.$fs.exists(pluginConfigPath)) { - const config = this.$projectConfigService.readConfig(plugin.fullPath); + // Plugin packages may ship compiled .js artifacts next to their + // .ts config; the dual-config warning is guidance for the user's + // own project and would be misleading here. + const config = this.$projectConfigService.readConfig( + plugin.fullPath, + { suppressWarnings: true }, + ); const packages = _.get( config, `${platformData.platformNameLowerCase}.SPMPackages`, [], ); if (packages.length) { + for (const pkg of packages) { + // a plugin's local package path is naturally authored relative + // to the plugin itself, but the SPM service resolves relative + // paths against the app project dir. When the app-relative + // path doesn't exist (e.g. non-hoisted node_modules layouts), + // fall back to resolving against the plugin's own directory. + if ( + "path" in pkg && + pkg.path && + !path.isAbsolute(pkg.path) && + !this.$fs.exists(path.resolve(projectData.projectDir, pkg.path)) + ) { + const pluginRelativePath = path.resolve( + plugin.fullPath, + pkg.path, + ); + if (this.$fs.exists(pluginRelativePath)) { + this.$logger.trace( + `SPM: resolved plugin-relative package path for ${pkg.name}: ${pluginRelativePath}`, + ); + pkg.path = pluginRelativePath; + } + } + } pluginSpmPackages.push(...packages); } } @@ -1420,9 +1533,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ): Promise { const project = this.createPbxProj(projectData); const group = await this.getRootGroup(groupName, sourceFolderPath); + // pin the sources to the main app target: without an explicit target the + // underlying xcode lib picks whichever "Sources" build phase it finds + // first, which can be an extension target (e.g. a widget) once one + // exists — compiling plugin native code into extensions breaks their + // builds (and bloats them) since they lack the app's search paths. project.addPbxGroup(group.files, group.name, group.path, null, { isMain: true, filesRelativeToProject: true, + target: project.getFirstTarget()?.uuid, }); project.addToHeaderSearchPaths(group.path); const headerFiles = this.$fs.exists(sourceFolderPath) @@ -1453,14 +1572,12 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ ); const platformData = this.getPlatformData(projectData); const pbxProjPath = this.getPbxProjPath(projectData); - const addedExtensionsFromResources = - await this.$iOSExtensionsService.addExtensionsFromPath({ - extensionsFolderPath: resorcesExtensionsPath, - projectData, - platformData, - pbxProjPath, - }); - let addedExtensionsFromPlugins = false; + await this.$iOSExtensionsService.addExtensionsFromPath({ + extensionsFolderPath: resorcesExtensionsPath, + projectData, + platformData, + pbxProjPath, + }); for (const pluginIndex in pluginsData) { const pluginData = pluginsData[pluginIndex]; const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath( @@ -1471,21 +1588,12 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ pluginPlatformsFolderPath, constants.NATIVE_EXTENSION_FOLDER, ); - const addedExtensionFromPlugin = - await this.$iOSExtensionsService.addExtensionsFromPath({ - extensionsFolderPath: extensionPath, - projectData, - platformData, - pbxProjPath, - }); - addedExtensionsFromPlugins = - addedExtensionsFromPlugins || addedExtensionFromPlugin; - } - - if (addedExtensionsFromResources || addedExtensionsFromPlugins) { - this.$logger.warn( - "Let us know if there are other Extension features you'd like! https://github.com/NativeScript/NativeScript/issues", - ); + await this.$iOSExtensionsService.addExtensionsFromPath({ + extensionsFolderPath: extensionPath, + projectData, + platformData, + pbxProjPath, + }); } } @@ -1635,6 +1743,19 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ project.removeFromHeaderSearchPaths({ relativePath: relativeHeaderSearchPath, }); + + // Remove the generated modulemap dir search path (see addStaticLibrary) + const modulemapDir = path.join( + this.getPlatformData(projectData).projectRoot, + IOSProjectService.GENERATED_PLUGINS_DIR_NAME, + path.basename(staticLibPath, ".a"), + ); + project.removeFromHeaderSearchPaths({ + relativePath: this.getLibSubpathRelativeToProjectPath( + modulemapDir, + projectData, + ), + }); }, ); @@ -1644,29 +1765,52 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private generateModulemap( headersFolderPath: string, libraryName: string, - ): void { + modulemapDir: string, + ): boolean { + const modulemapPath = path.join(modulemapDir, "module.modulemap"); + + // A plugin may ship a `.a` without an `include/{lib}` headers folder. In + // that case there's nothing to expose as a module - clean up any stale + // modulemap and bail out instead of letting readDirectory throw. + if (!this.$fs.exists(headersFolderPath)) { + if (this.$fs.exists(modulemapPath)) { + this.$fs.deleteFile(modulemapPath); + } + return false; + } + const headersFilter = (fileName: string, containingFolderPath: string) => path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile(); const headersFolderContents = this.$fs.readDirectory(headersFolderPath); - let headers = _(headersFolderContents) - .filter((item) => headersFilter(item, headersFolderPath)) - .value(); + const headerFiles = headersFolderContents.filter((item) => + headersFilter(item, headersFolderPath), + ); - if (!headers.length) { - this.$fs.deleteFile(path.join(headersFolderPath, "module.modulemap")); - return; + if (!headerFiles.length) { + if (this.$fs.exists(modulemapPath)) { + this.$fs.deleteFile(modulemapPath); + } + return false; } - headers = _.map(headers, (value) => `header "${value}"`); + // Reference the plugin's headers (still in node_modules) relative to the + // generated modulemap's location, so we don't copy headers or write into + // node_modules. + const headers = _.map(headerFiles, (value) => { + const relativeHeaderPath = path.relative( + modulemapDir, + path.join(headersFolderPath, value), + ); + return `header "${relativeHeaderPath}"`; + }); const modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join( " ", )} } }`; - this.$fs.writeFile( - path.join(headersFolderPath, "module.modulemap"), - modulemap, - ); + this.$fs.ensureDirectoryExists(modulemapDir); + this.$fs.writeFile(modulemapPath, modulemap); + return true; } private async mergeProjectXcconfigFiles( @@ -1683,6 +1827,23 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.$fs.deleteFile(pluginsXcconfigFilePath); } + // mergeFiles keeps whichever value is already present, so the app's + // xcconfig is merged before any plugin's to make it authoritative: a + // plugin must not be able to dictate a setting the app has chosen. + const appResourcesXcconfigPath = path.join( + projectData.appResourcesDirectoryPath, + this.getPlatformData(projectData).normalizedPlatformName, + BUILD_XCCONFIG_FILE_NAME, + ); + if (this.$fs.exists(appResourcesXcconfigPath)) { + for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { + await this.$xcconfigService.mergeFiles( + appResourcesXcconfigPath, + pluginsXcconfigFilePath, + ); + } + } + const allPlugins: IPluginData[] = this.getAllProductionPlugins(projectData); for (const plugin of allPlugins) { const pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath( @@ -1702,20 +1863,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } } - const appResourcesXcconfigPath = path.join( - projectData.appResourcesDirectoryPath, - this.getPlatformData(projectData).normalizedPlatformName, - BUILD_XCCONFIG_FILE_NAME, - ); - if (this.$fs.exists(appResourcesXcconfigPath)) { - for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { - await this.$xcconfigService.mergeFiles( - appResourcesXcconfigPath, - pluginsXcconfigFilePath, - ); - } - } - for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { if (!this.$fs.exists(pluginsXcconfigFilePath)) { // We need the pluginsXcconfig file to exist in platforms dir as it is required in the native template: diff --git a/lib/services/ios-watch-app-service.ts b/lib/services/ios-watch-app-service.ts index 27bd57dd76..e6112d4cf5 100644 --- a/lib/services/ios-watch-app-service.ts +++ b/lib/services/ios-watch-app-service.ts @@ -12,162 +12,1461 @@ import { IAddWatchAppFromPathOptions, IRemoveWatchAppOptions, IProjectData, + IXcodeTargetBuildConfigurationProperty, + IWatchAppJSONConfig, + IWatchAppJSONConfigModule, } from "../definitions/project"; import { IPlatformData } from "../definitions/platform"; import { IFileSystem } from "../common/declarations"; import { injector } from "../common/yok"; +import { Minimatch } from "minimatch"; + +const sourceExtensions = [ + ".swift", + ".m", + ".mm", + ".c", + ".cpp", + ".cc", + ".cxx", + ".h", + ".hpp", +]; +const resourceExtensions = [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".svg", + ".pdf", // Images + ".ttf", + ".otf", + ".woff", + ".woff2", // Fonts + ".xcassets", // Asset catalogs + ".storyboard", + ".xib", // Interface files + ".strings", + ".stringsdict", // Localization + ".json", + ".xml", + ".plist", // Data files + ".m4a", + ".mp3", + ".wav", + ".caf", // Audio + ".mp4", + ".mov", // Video + ".bundle", // Resource bundles +]; +const WATCH_APP_IDENTIFIER = "watchkitapp"; +const WACTCH_EXTENSION_IDENTIFIER = "watchkitextension"; +const CONFIG_FILE_WATCHAPP = "watchapp.json"; +const CONFIG_FILE_EXTENSION = "extension.json"; +const RESOURCES_TO_IGNORE = [ + CONFIG_FILE_WATCHAPP, + CONFIG_FILE_EXTENSION, + "node_modules", +]; export class IOSWatchAppService implements IIOSWatchAppService { - private static WATCH_APP_IDENTIFIER = "watchkitapp"; - private static WACTCH_EXTENSION_IDENTIFIER = "watchkitextension"; constructor( protected $fs: IFileSystem, protected $pbxprojDomXcode: IPbxprojDomXcode, protected $xcode: IXcode, - private $iOSNativeTargetService: IIOSNativeTargetService + private $iOSNativeTargetService: IIOSNativeTargetService, + private $logger: ILogger, + private $spmPbxprojService: ISPMPbxprojService, ) {} + private addResourceFile( + project: IXcode.project, + path: string, + opt: Record, + group = "WatchResources", + ) { + const file = (project as any).addResourceFile(path, opt, group); + (project as any).addToResourcesPbxGroup(file, group); + } + private addSourceFile( + project: IXcode.project, + path: string, + opt: Record, + group = "WatchSrc", + ) { + const file = (project as any).addSourceFile(path, opt, group); + (project as any).addToResourcesPbxGroup(file, group); + } + public async addWatchAppFromPath({ watchAppFolderPath, projectData, platformData, pbxProjPath, + disableStubBinary = false, }: IAddWatchAppFromPathOptions): Promise { const targetUuids: string[] = []; + const targetNames: string[] = []; const appPath = path.join(watchAppFolderPath, IOS_WATCHAPP_FOLDER); const extensionPath = path.join( watchAppFolderPath, - IOS_WATCHAPP_EXTENSION_FOLDER + IOS_WATCHAPP_EXTENSION_FOLDER, ); + const hasWatchExtension = this.$fs.exists(extensionPath); - if (!this.$fs.exists(appPath) || !this.$fs.exists(extensionPath)) { + // Check if watchapp exists - it's required + if (!this.$fs.exists(appPath)) { return false; } - const appFolder = this.$iOSNativeTargetService.getTargetDirectories( - appPath - )[0]; - const extensionFolder = this.$iOSNativeTargetService.getTargetDirectories( - extensionPath - )[0]; + const appFolder = + this.$iOSNativeTargetService.getTargetDirectories(appPath)[0]; const project = new this.$xcode.project(pbxProjPath); project.parseSync(); - const watchApptarget = this.$iOSNativeTargetService.addTargetToProject( + const configPath = path.join( + path.join(appPath, appFolder), + "watchapp.json", + ); + const config: IWatchAppJSONConfig = this.$fs.exists(configPath) + ? this.$fs.readJson(configPath) + : null; + + const targetType = config?.targetType ?? IOSNativeTargetTypes.watchApp; + project.removeTargetsByProductType(IOSNativeTargetProductTypes.watchApp); + project.removeTargetsByProductType(targetType); + + const parentTargetUuid = project.getFirstTarget().uuid; + + const watchApptarget = this.addTarget( appPath, appFolder, - IOSNativeTargetTypes.watchApp, + targetType, project, platformData, - project.getFirstTarget().uuid + parentTargetUuid, + IOSNativeTargetTypes.watchApp, ); - this.configureTarget( + + await this.configureTarget( appFolder, path.join(appPath, appFolder), - `${projectData.projectIdentifiers.ios}.${IOSWatchAppService.WATCH_APP_IDENTIFIER}`, - "watchapp.json", + `${projectData.projectIdentifiers.ios}.${WATCH_APP_IDENTIFIER}`, + configPath, + config, watchApptarget, - project - ); - targetUuids.push(watchApptarget.uuid); - - const watchExtensionTarget = this.$iOSNativeTargetService.addTargetToProject( - extensionPath, - extensionFolder, - IOSNativeTargetTypes.watchExtension, project, + projectData, platformData, - watchApptarget.uuid - ); - this.configureTarget( - extensionFolder, - path.join(extensionPath, extensionFolder), - `${projectData.projectIdentifiers.ios}.${IOSWatchAppService.WATCH_APP_IDENTIFIER}.${IOSWatchAppService.WACTCH_EXTENSION_IDENTIFIER}`, - "extension.json", - watchExtensionTarget, - project + pbxProjPath, + !hasWatchExtension, ); - targetUuids.push(watchExtensionTarget.uuid); + targetUuids.push(watchApptarget.uuid); + targetNames.push(appFolder); + + // Extension is optional (Xcode 14+ supports single target) + if (hasWatchExtension) { + const extensionFolder = + this.$iOSNativeTargetService.getTargetDirectories(extensionPath)[0]; + const configPath = path.join( + path.join(extensionPath, extensionFolder), + "extension.json", + ); + + const config = this.$fs.exists(configPath) + ? this.$fs.readJson(configPath) + : null; + + const targetType = + config?.targetType ?? IOSNativeTargetTypes.watchExtension; + project.removeTargetsByProductType( + IOSNativeTargetProductTypes.watchExtension, + ); + project.removeTargetsByProductType(targetType); + + const watchExtensionTarget = this.addTarget( + extensionPath, + extensionFolder, + targetType, + project, + platformData, + watchApptarget.uuid, + ); + + await this.configureTarget( + extensionFolder, + path.join(extensionPath, extensionFolder), + `${projectData.projectIdentifiers.ios}.${WATCH_APP_IDENTIFIER}.${WACTCH_EXTENSION_IDENTIFIER}`, + configPath, + config, + watchExtensionTarget, + project, + projectData, + platformData, + pbxProjPath, + ); + targetUuids.push(watchExtensionTarget.uuid); + targetNames.push(extensionFolder); + } else { + this.$logger.debug( + "No watch extension found - using single target mode (Xcode 14+)", + ); + } this.$fs.writeFile( pbxProjPath, - project.writeSync({ omitEmptyValues: true }) + project.writeSync({ omitEmptyValues: true }), + ); + + // Add SPM packages (file needs to be saved first) + const watchSPMPackages = this.getWatchSPMPackages(platformData); + + await this.applySPMPackagesToTargets( + targetNames, + platformData, + projectData.projectDir, + watchSPMPackages, ); + // nothing done after we dont need to reload project + this.$iOSNativeTargetService.prepareSigning( targetUuids, projectData, - pbxProjPath + pbxProjPath, ); + if (disableStubBinary) { + this.applyWatchAppStubBinaryOverrides(appFolder, pbxProjPath); + } + return true; } + private addTarget( + targetRootPath: string, + targetFolder: string, + targetType: string, + project: IXcode.project, + platformData: IPlatformData, + parentTarget?: string, + productTargetType?: string, + ): IXcode.target { + const targetPath = path.join(targetRootPath, targetFolder); + const targetRelativePath = path.relative( + platformData.projectRoot, + targetPath, + ); + + const target = project.addTarget( + targetFolder, + targetType, + targetRelativePath, + parentTarget, + productTargetType, + ); + + // Add build phases + project.addBuildPhase([], "PBXSourcesBuildPhase", "Sources", target.uuid); + project.addBuildPhase( + [], + "PBXResourcesBuildPhase", + "Resources", + target.uuid, + ); + project.addBuildPhase( + [], + "PBXFrameworksBuildPhase", + "Frameworks", + target.uuid, + ); + project.addBuildPhase( + [], + "PBXCopyFilesBuildPhase", + "Embed Frameworks", + target.uuid, + "frameworks", + ); + + project.addToHeaderSearchPaths( + targetPath, + target.pbxNativeTarget.productName, + ); + + return target; + } + + /** + * Recursively add source files from a directory to a target + */ + private addSourceFilesFromDirectory( + dirPath: string, + targetUuid: string, + project: IXcode.project, + platformData: IPlatformData, + groupName: string, + excludePatterns?: string[], + ): void { + const items = this.getFolderFiles( + dirPath, + platformData.projectRoot, + excludePatterns, + ); + + for (const item of items) { + const relativePath = path.relative(platformData.projectRoot, item); + // Check if file is a source file by extension + const ext = path.extname(item).toLowerCase(); + if (sourceExtensions.includes(ext)) { + this.$logger.debug(`Adding source file: ${relativePath}`); + this.addSourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + } + + private async addTargetResources( + watchAppFolderPath: string, + targetUuids: string[], + project: IXcode.project, + platformData: IPlatformData, + groupName: string, + excludePatterns?: string[], + ): Promise { + try { + if (!this.$fs.exists(watchAppFolderPath)) { + return; + } + for (let i = 0; i < targetUuids.length; i++) { + const targetUuid = targetUuids[i]; + this.addResourcesFromDirectory( + watchAppFolderPath, + targetUuid, + project, + platformData, + groupName, + excludePatterns, + ); + } + + this.$logger.debug("Watch app resources added successfully"); + } catch (err) { + this.$logger.warn(`Error adding watch app resources: ${err.message}`); + } + } + + /** + * Recursively add resources from a directory to a target + */ + private addResourcesFromDirectory( + dirPath: string, + targetUuid: string, + project: IXcode.project, + platformData: IPlatformData, + groupName: string, + excludePatterns?: string[], + ): void { + const items = this.$fs.readDirectory(dirPath); + + for (const item of items) { + // Skip hidden files and excluded files/directories + if (item.startsWith(".") || RESOURCES_TO_IGNORE.indexOf(item) !== -1) { + continue; + } + + const itemPath = path.join(dirPath, item); + const stats = this.$fs.getFsStats(itemPath); + const relativePath = path.relative(platformData.projectRoot, itemPath); + + // Check if file/directory should be excluded based on patterns + if ( + excludePatterns && + this.shouldExclude(relativePath, excludePatterns) + ) { + this.$logger.debug(`Excluding from resources: ${relativePath}`); + continue; + } + + if (stats.isDirectory()) { + // Special handling for .xcassets, .bundle, and other resource bundles + if (item.endsWith(".xcassets") || item.endsWith(".bundle")) { + this.$logger.debug(`Adding resource bundle: ${relativePath}`); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } else { + // Recursively scan subdirectories + this.addResourcesFromDirectory( + itemPath, + targetUuid, + project, + platformData, + groupName, + excludePatterns, + ); + } + } else { + // Check if file is a resource by extension + const ext = path.extname(item).toLowerCase(); + if (resourceExtensions.includes(ext)) { + this.$logger.debug(`Adding resource file: ${relativePath}`); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + } + } + public removeWatchApp({ pbxProjPath }: IRemoveWatchAppOptions): void { const project = new this.$xcode.project(pbxProjPath); project.parseSync(); project.removeTargetsByProductType(IOSNativeTargetProductTypes.watchApp); project.removeTargetsByProductType( - IOSNativeTargetProductTypes.watchExtension + IOSNativeTargetProductTypes.watchExtension, ); this.$fs.writeFile( pbxProjPath, - project.writeSync({ omitEmptyValues: true }) + project.writeSync({ omitEmptyValues: true }), ); } public hasWatchApp( platformData: IPlatformData, - projectData: IProjectData + projectData: IProjectData, ): boolean { const watchAppPath = path.join( projectData.getAppResourcesDirectoryPath(), platformData.normalizedPlatformName, - IOS_WATCHAPP_FOLDER + IOS_WATCHAPP_FOLDER, ); return this.$fs.exists(watchAppPath); } - private configureTarget( + private async configureTarget( targetName: string, targetPath: string, identifier: string, - configurationFileName: string, + configPath: string, + config: IWatchAppJSONConfig, target: IXcode.target, - project: IXcode.project + project: IXcode.project, + projectData: IProjectData, + platformData: IPlatformData, + pbxProjPath: string, + disableStubBinary = false, ) { - const targetConfigurationJsonPath = path.join( - targetPath, - configurationFileName - ); - const identifierParts = identifier.split("."); identifierParts.pop(); const wkAppBundleIdentifier = identifierParts.join("."); + // Build configuration properties + const buildConfigProperties: IXcodeTargetBuildConfigurationProperty[] = [ + { name: "PRODUCT_BUNDLE_IDENTIFIER", value: identifier }, + { name: "SDKROOT", value: "watchos" }, + { name: "TARGETED_DEVICE_FAMILY", value: IOSDeviceTargets.watchos }, + { name: "WATCHOS_DEPLOYMENT_TARGET", value: 5.2 }, + { name: "WK_APP_BUNDLE_IDENTIFIER", value: wkAppBundleIdentifier }, + ]; + + if (disableStubBinary) { + buildConfigProperties.push( + { name: "PRODUCT_BINARY_SOURCE_PATH", value: '""' }, + { name: "PRODUCT_TYPE_HAS_STUB_BINARY", value: "NO" }, + ); + } + const resourcesGroup = targetName + "Resources"; + project.addPbxGroup([], resourcesGroup, project.filepath, null, { + isMain: true, + target: target.uuid, + filesRelativeToProject: true, + }); + const srcGroup = targetName + "Src"; + project.addPbxGroup([], srcGroup, project.filepath, null, { + isMain: true, + target: target.uuid, + filesRelativeToProject: true, + }); + + let basedir: string | undefined; + if (config?.basedir) { + basedir = path.resolve(path.dirname(configPath), config.basedir); + if (!this.$fs.exists(basedir)) { + this.$logger.warn( + `Basedir not found, using config directory: ${basedir}`, + ); + basedir = path.dirname(configPath); + } + } else { + basedir = path.dirname(configPath); + } + + const resourcesExclude = config?.resourcesExclude || []; + const srcExclude = config?.srcExclude || []; + + // Handle custom Info.plist path + if (config?.infoPlistPath) { + const infoPlistPath = path.resolve(basedir, config.infoPlistPath); + if (this.$fs.exists(infoPlistPath)) { + const relativeInfoPlistPath = path.relative( + platformData.projectRoot, + infoPlistPath, + ); + buildConfigProperties.push({ + name: "INFOPLIST_FILE", + value: `"${infoPlistPath}"`, + }); + resourcesExclude.push(relativeInfoPlistPath); + } else { + this.$logger.warn(`Custom Info.plist not found at: ${infoPlistPath}`); + } + } + + // Handle custom xcprivacy file path + if (config?.xcprivacyPath) { + const xcprivacyPath = path.resolve(basedir, config.xcprivacyPath); + if (this.$fs.exists(xcprivacyPath)) { + const relativeXcprivacyPath = path.relative( + platformData.projectRoot, + xcprivacyPath, + ); + this.addResourceFile( + project, + xcprivacyPath, + { target: target.uuid }, + targetName + "Resources", + ); + resourcesExclude.push(relativeXcprivacyPath); + } else { + this.$logger.warn( + `Custom xcprivacy file not found at: ${xcprivacyPath}`, + ); + } + } + this.$iOSNativeTargetService.setXcodeTargetBuildConfigurationProperties( - [ - { name: "PRODUCT_BUNDLE_IDENTIFIER", value: identifier }, - { name: "SDKROOT", value: "watchos" }, - { name: "TARGETED_DEVICE_FAMILY", value: IOSDeviceTargets.watchos }, - { name: "WATCHOS_DEPLOYMENT_TARGET", value: 5.2 }, - { name: "WK_APP_BUNDLE_IDENTIFIER", value: wkAppBundleIdentifier }, - ], + buildConfigProperties, targetName, - project + project, ); this.$iOSNativeTargetService.setConfigurationsFromJsonFile( - targetConfigurationJsonPath, + configPath, target.uuid, targetName, - project + project, ); project.addToHeaderSearchPaths( targetPath, - target.pbxNativeTarget.productName + target.pbxNativeTarget.productName, ); + + if (config?.importSourcesFromMainFolder !== false) { + await this.addSourceFilesFromDirectory( + path.dirname(configPath), + target.uuid, + project, + platformData, + targetName + "Src", + srcExclude, + ); + } + + if (config?.importResourcesFromMainFolder !== false) { + await this.addTargetResources( + path.dirname(configPath), + [target.uuid], + project, + platformData, + resourcesGroup, + resourcesExclude, + ); + } + + if (config) { + // Process additional configurations + await this.processWatchAppConfiguration( + config, + basedir, + targetName, + target, + project, + projectData, + platformData, + pbxProjPath, + srcExclude, + resourcesExclude, + ); + } + } + + private applyWatchAppStubBinaryOverrides( + targetName: string, + pbxProjPath: string, + ): void { + const project = new this.$xcode.project(pbxProjPath); + project.parseSync(); + + this.$iOSNativeTargetService.setXcodeTargetBuildConfigurationProperties( + [ + { name: "PRODUCT_BINARY_SOURCE_PATH", value: '""' }, + { name: "PRODUCT_TYPE_HAS_STUB_BINARY", value: "NO" }, + ], + targetName, + project, + ); + + this.$fs.writeFile( + pbxProjPath, + project.writeSync({ omitEmptyValues: true }), + ); + } + + private async processWatchAppConfiguration( + config: IWatchAppJSONConfig, + basedir: string, + targetName: string, + target: IXcode.target, + project: IXcode.project, + projectData: IProjectData, + platformData: IPlatformData, + pbxProjPath: string, + srcExclude: string[], + resourcesExclude: string[], + ): Promise { + this.$logger.debug( + `processWatchAppConfiguration ${JSON.stringify(config)}`, + ); + + // Handle custom resources + if (config.resources && Array.isArray(config.resources)) { + this.$logger.debug( + `Processing ${config.resources.length} custom resource(s) for watch target: ${targetName}`, + ); + for (const resourcePath of config.resources) { + this.addCustomResource( + resourcePath, + target.uuid, + project, + projectData, + platformData, + targetName + "Resources", + resourcesExclude, + basedir, + ); + } + } + + if (config.src && Array.isArray(config.src)) { + this.$logger.debug( + `Processing ${config.src.length} custom source file(s) for watch target: ${targetName}`, + ); + for (const srcPath of config.src) { + this.addCustomSourceFile( + srcPath, + target.uuid, + project, + projectData, + platformData, + srcExclude, + targetName + "Src", + basedir, + ); + } + } + + if (config.SPMPackages && Array.isArray(config.SPMPackages)) { + // to be able to add SPM the file needs to be saved + // but it means we need to reload it again after spm packages addition + this.$fs.writeFile( + pbxProjPath, + project.writeSync({ omitEmptyValues: true }), + ); + await this.applySPMPackagesToTargets( + [targetName], + platformData, + basedir, + config.SPMPackages, + ); + project.parseSync(); + } + + if (config.modules && Array.isArray(config.modules)) { + this.$logger.debug( + `Processing ${config.modules.length} module(s) for watch target: ${targetName}`, + ); + for (const moduleDef of config.modules) { + await this.addModuleDependency( + moduleDef, + config, + targetName, + target, + project, + projectData, + platformData, + srcExclude, + resourcesExclude, + basedir, + ); + } + } + } + + private addCustomResource( + resourcePath: string, + targetUuid: string, + project: IXcode.project, + projectData: IProjectData, + platformData: IPlatformData, + groupName: string, + excludePatterns: string[], + basedir?: string, + ): void { + const resolvedPath = this.resolvePathWithBasedir( + resourcePath, + basedir, + projectData.projectDir, + ); + + if (!this.$fs.exists(resolvedPath)) { + this.$logger.warn(`Custom resource not found, skipping: ${resourcePath}`); + return; + } + + const relativePath = path.relative(platformData.projectRoot, resolvedPath); + + if (excludePatterns && this.shouldExclude(relativePath, excludePatterns)) { + this.$logger.debug(`Excluding from resources: ${relativePath}`); + return; + } + const stats = this.$fs.getFsStats(resolvedPath); + + if (stats.isDirectory()) { + this.$logger.debug( + `Recursively adding files from resource directory: ${resourcePath}`, + ); + if ( + relativePath.endsWith(".xcassets") || + relativePath.endsWith(".bundle") + ) { + this.$logger.debug( + `Adding resource bundle: ${relativePath} for target:${targetUuid}`, + ); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } else { + this.addAllResourcesRecursively( + resolvedPath, + targetUuid, + project, + platformData, + groupName, + excludePatterns, + ); + } + } else { + this.$logger.debug(`Adding custom resource file: ${relativePath}`); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + + private addCustomSourceFile( + srcPath: string, + targetUuid: string, + project: IXcode.project, + projectData: IProjectData, + platformData: IPlatformData, + excludePatterns: string[], + groupName: string, + basedir?: string, + ): void { + const resolvedPath = this.resolvePathWithBasedir( + srcPath, + basedir, + projectData.projectDir, + ); + + if (!this.$fs.exists(resolvedPath)) { + this.$logger.warn( + `Custom source file/folder not found, skipping: ${srcPath}`, + ); + return; + } + + const relativePath = path.relative(platformData.projectRoot, resolvedPath); + + if (excludePatterns && this.shouldExclude(relativePath, excludePatterns)) { + this.$logger.debug(`Excluding from src: ${relativePath}`); + return; + } + + const stats = this.$fs.getFsStats(resolvedPath); + + if (stats.isDirectory()) { + this.$logger.debug(`Adding custom source directory: ${relativePath}`); + this.addAllSourceFilesFromDirectory( + resolvedPath, + targetUuid, + project, + platformData, + groupName, + excludePatterns, + ); + } else { + this.$logger.debug(`Adding custom source file: ${relativePath}`); + this.addSourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + + private resolvePathWithBasedir( + relativePath: string, + basedir: string | undefined, + fallbackDir: string, + ): string { + return basedir + ? path.resolve(basedir, relativePath) + : path.resolve(fallbackDir, relativePath); + } + + private addAllSourceFilesFromDirectory( + dirPath: string, + targetUuid: string, + project: IXcode.project, + platformData: IPlatformData, + groupName: string, + excludePatterns: string[], + ): void { + const items = this.getFolderFiles( + dirPath, + platformData.projectRoot, + excludePatterns, + ); + + for (const item of items) { + const relativePath = path.relative(platformData.projectRoot, item); + // Check if file is a source file by extension + const ext = path.extname(item).toLowerCase(); + if (sourceExtensions.includes(ext)) { + this.$logger.debug(`Adding source file: ${relativePath}`); + this.addSourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + } + + private addAllResourcesRecursively( + dirPath: string, + targetUuid: string, + project: IXcode.project, + platformData: IPlatformData, + groupName: string, + excludePatterns: string[], + ): void { + const items = this.$fs.readDirectory(dirPath); + + for (const item of items) { + if (item.startsWith(".")) { + continue; + } + + const itemPath = path.join(dirPath, item); + const stats = this.$fs.getFsStats(itemPath); + const relativePath = path.relative(platformData.projectRoot, itemPath); + + if ( + excludePatterns && + this.shouldExclude(relativePath, excludePatterns) + ) { + this.$logger.debug(`Excluding from resources: ${relativePath}`); + return; + } + + if (stats.isDirectory()) { + // Special handling for .xcassets, .bundle - add as bundles, not recursively + if (item.endsWith(".xcassets") || item.endsWith(".bundle")) { + this.$logger.debug( + `Adding resource bundle: ${relativePath} for target:${targetUuid}`, + ); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } else { + this.addAllResourcesRecursively( + itemPath, + targetUuid, + project, + platformData, + groupName, + excludePatterns, + ); + } + } else { + this.$logger.debug(`Adding resource file: ${relativePath}`); + this.addResourceFile( + project, + relativePath, + { target: targetUuid }, + groupName, + ); + } + } + } + + private async addModuleDependency( + moduleDef: IWatchAppJSONConfigModule, + config: IWatchAppJSONConfig, + targetName: string, + target: IXcode.target, + project: IXcode.project, + projectData: IProjectData, + platformData: IPlatformData, + srcExclude: string[], + resourcesExclude: string[], + basedir?: string, + ): Promise { + const modulePath = moduleDef.path + ? this.resolvePathWithBasedir( + moduleDef.path, + basedir, + projectData.projectDir, + ) + : null; + + if (!modulePath || !this.$fs.exists(modulePath)) { + this.$logger.warn( + `Module path not found, skipping module: ${modulePath}`, + ); + return; + } + + const relativePath = path.relative(platformData.projectRoot, modulePath); + const stats = this.$fs.getFsStats(modulePath); + + const isFramework = + modulePath.endsWith(".framework") || modulePath.endsWith(".xcframework"); + const isFolder = stats.isDirectory() && !isFramework; + this.$logger.debug( + `Adding module dependency: ${JSON.stringify(moduleDef)} to ${targetName}, basedir:${basedir}, isFramework:${isFramework} isFolder:${isFolder}`, + ); + + if (isFramework) { + // Handle compiled frameworks (xcframework, framework) + this.addCompiledFramework( + moduleDef, + relativePath, + targetName, + target, + project, + ); + } else if (isFolder) { + // Handle folder-based modules + await this.addFolderModule( + moduleDef, + modulePath, + relativePath, + targetName, + target, + config, + project, + basedir, + srcExclude, + resourcesExclude, + projectData, + platformData, + ); + } else { + this.$logger.warn(`Unknown module type for: ${modulePath}`); + } + + if ( + moduleDef.headerSearchPaths && + Array.isArray(moduleDef.headerSearchPaths) + ) { + for (const headerPath of moduleDef.headerSearchPaths) { + const resolvedPath = this.resolvePathWithBasedir( + headerPath, + basedir, + projectData.projectDir, + ); + const relPath = path.relative(platformData.projectRoot, resolvedPath); + project.addToHeaderSearchPaths(relPath, targetName); + this.$logger.debug(`Added header search path: ${relPath}`); + } + } + + if (moduleDef.linkerFlags && Array.isArray(moduleDef.linkerFlags)) { + this.addLinkerFlags(moduleDef.linkerFlags, targetName, project); + } + } + + private addCompiledFramework( + moduleDef: any, + relativePath: string, + targetName: string, + target: IXcode.target, + project: IXcode.project, + ): void { + const moduleName = moduleDef.name; + + project.addFramework(relativePath, { + target: target.uuid, + customFramework: true, + embed: moduleDef.embed !== false, // Default to true + }); + + const frameworkDir = path.dirname(relativePath); + project.addBuildProperty( + "FRAMEWORK_SEARCH_PATHS", + `"$(inherited)" "${frameworkDir}"`, + null, + targetName, + ); + + this.$logger.debug( + `Added compiled framework ${moduleName} at ${relativePath}`, + ); + } + + private getFolderFiles( + dirPath: string, + rootPath: string, + excludePatterns?: string[], + ) { + const result: string[] = []; + const files = this.$fs + .readDirectory(dirPath) + .filter((fileName) => !fileName.startsWith(".")); + for (const item of files) { + const itemPath = path.join(dirPath, item); + const stats = this.$fs.getFsStats(itemPath); + const relativePath = path.relative(rootPath, itemPath); + + if ( + excludePatterns && + this.shouldExclude(relativePath, excludePatterns) + ) { + this.$logger.debug(`Excluding from src: ${relativePath}`); + continue; + } + if (stats.isDirectory()) { + result.push( + ...this.getFolderFiles(itemPath, rootPath, excludePatterns), + ); + } else { + result.push(itemPath); + } + } + return result; + } + + addBuildPhaseIfNotExisting( + project: IXcode.project, + buildPhaseType: string, + comment: string, + target: string, + ) { + let buildPhase = project.buildPhaseObject(buildPhaseType, comment, target); + if (!buildPhase) { + project.addBuildPhase([], buildPhaseType, comment, target); + } + } + + private async addFolderModule( + moduleDef: IWatchAppJSONConfigModule, + modulePath: string, + relativePath: string, + targetName: string, + target: IXcode.target, + config: IWatchAppJSONConfig, + project: IXcode.project, + basedir: string, + srcExclude: string[], + resourcesExclude: string[], + projectData: IProjectData, + platformData: IPlatformData, + ): Promise { + const moduleName = moduleDef.name || path.basename(modulePath); + + const targetRelativePath = path.relative( + platformData.projectRoot, + modulePath, + ); + const moduleTarget = project.addTarget( + moduleName, + moduleDef.targetType ?? "framework", + targetRelativePath, + target.uuid, + ); + this.$logger.debug( + `Adding folder module ${moduleName} with path ${modulePath} with target uuid:${moduleTarget.uuid}`, + ); + + const { + path: filePath, + name, + dependencies, + frameworks, + buildConfigurationProperties, + src, + resources, + SPMPackages, + ...otherProps + } = moduleDef; + project.addFramework(moduleName + ".framework", { + target: target.uuid, + basename: moduleName, + path: moduleName + ".framework", + customFramework: true, + explicitFileType: "wrapper.framework", + ...otherProps, + }); + + // Add build phases + project.addBuildPhase( + [], + "PBXSourcesBuildPhase", + "Sources", + moduleTarget.uuid, + ); + project.addBuildPhase( + [], + "PBXResourcesBuildPhase", + "Resources", + moduleTarget.uuid, + ); + project.addBuildPhase( + [], + "PBXFrameworksBuildPhase", + "Frameworks", + moduleTarget.uuid, + ); + project.addBuildPhase( + [], + "PBXCopyFilesBuildPhase", + "Embed Frameworks", + moduleTarget.uuid, + "frameworks", + ); + + const files = this.getFolderFiles( + modulePath, + platformData.projectRoot, + srcExclude, + ); + this.$logger.debug(`module ${moduleName} has ${files.length} files`); + if (files.length > 0) { + project.addPbxGroup(files, moduleName, modulePath, null, { + isMain: true, + target: moduleTarget.uuid, + filesRelativeToProject: true, + }); + } + + if (moduleDef.frameworks && Array.isArray(moduleDef.frameworks)) { + this.$logger.debug( + `Adding ${moduleDef.frameworks.length} framework(s) for module ${JSON.stringify(moduleDef)}`, + ); + for (const framework of moduleDef.frameworks) { + this.$logger.debug( + `Adding framework ${JSON.stringify(framework)} for module ${JSON.stringify(moduleDef)}`, + ); + if (typeof framework === "string") { + project.addFramework(framework, { target: moduleTarget.uuid }); + } else { + project.addFramework(framework.path, { + target: moduleTarget.uuid, + ...framework, + }); + } + this.$logger.debug(`Added framework dependency: ${framework}`); + } + } + + if (moduleDef.src && Array.isArray(moduleDef.src)) { + this.$logger.debug( + `Processing ${config.src.length} custom source file(s) for target: ${moduleName}`, + ); + for (const srcPath of moduleDef.src) { + this.addCustomSourceFile( + srcPath, + moduleTarget.uuid, + project, + projectData, + platformData, + srcExclude, + moduleName + "Src", + basedir, + ); + } + } + + if (moduleDef.resources && Array.isArray(moduleDef.resources)) { + this.$logger.debug( + `Processing ${moduleDef.resources.length} custom resource(s) for target: ${moduleName}/${moduleTarget.uuid}`, + ); + for (const resourcePath of moduleDef.resources) { + this.addCustomResource( + resourcePath, + moduleTarget.uuid, + project, + projectData, + platformData, + targetName + "Resources", + resourcesExclude, + basedir, + ); + } + } + + if (moduleDef.dependencies && Array.isArray(moduleDef.dependencies)) { + const currentTargets = project.pbxNativeTargetSection(); + const currentTargetsArray = Object.keys(currentTargets) + .map((k) => + currentTargets[k]["name"] + ? { uuid: k, name: currentTargets[k]["name"] } + : null, + ) + .filter((t) => !!t); + const targets = moduleDef.dependencies + .map((dependency) => + currentTargetsArray.find((t) => t.name === `\"${dependency}\"`), + ) + .filter((s) => !!s); + if (targets.length) { + this.$logger.debug( + `Adding target dependencies ${moduleDef.dependencies} with uuids:${targets.map((t) => t.uuid)} for module ${moduleDef.name}`, + ); + project.addTargetDependency( + moduleTarget.uuid, + targets.map((t) => t.uuid), + ); + } + } + + if (moduleDef.SPMPackages && Array.isArray(moduleDef.SPMPackages)) { + // to be able to add SPM the file needs to be saved + // but it means we need to reload it again after spm packages addition + this.$fs.writeFile( + project.filepath, + project.writeSync({ omitEmptyValues: true }), + ); + await this.applySPMPackagesToTargets( + [moduleName], + platformData, + basedir, + moduleDef.SPMPackages.map((t) => { + if (typeof t === "string") { + return config.SPMPackages.find((s) => s.name === t); + } + return t; + }), + ); + project.parseSync(); + } + + if ( + moduleDef.buildConfigurationProperties || + config.sharedModulesBuildConfigurationProperties + ) { + const configurationProperties = { + ...(config.sharedModulesBuildConfigurationProperties || {}), + ...(moduleDef.buildConfigurationProperties || {}), + }; + this.$iOSNativeTargetService.setXcodeTargetBuildConfigurationProperties( + Object.keys(configurationProperties).map((k) => ({ + name: k, + value: configurationProperties[k], + })), + moduleName, + project, + ); + } + + this.$logger.debug( + `Added folder-based module ${moduleName} at ${relativePath}`, + ); + } + + /** + * Add linker flags to a target's build settings + */ + private addLinkerFlags( + flags: string[], + targetName: string, + project: IXcode.project, + ): void { + for (const flag of flags) { + const currentFlags = this.getBuildProperty( + "OTHER_LDFLAGS", + targetName, + project, + ); + const flagsArray = currentFlags + ? Array.isArray(currentFlags) + ? currentFlags + : [currentFlags] + : ['"$(inherited)"']; + + if (!flagsArray.includes(flag)) { + flagsArray.push(flag); + } + + project.addBuildProperty("OTHER_LDFLAGS", flagsArray, null, targetName); + this.$logger.debug(`Added linker flag: ${flag}`); + } + } + + /** + * Get build property value for a specific target + */ + private getBuildProperty( + propertyName: string, + targetName: string, + project: IXcode.project, + ): any { + // Access the project hash to read build settings + const projectHash = (project as any).hash; + if (!projectHash) { + return null; + } + + const configurations = projectHash.project.objects.XCBuildConfiguration; + if (!configurations) { + return null; + } + + for (const key in configurations) { + const config = configurations[key]; + if ( + config && + config.buildSettings && + (config.buildSettings.PRODUCT_NAME === targetName || + config.buildSettings.PRODUCT_NAME === `"${targetName}"`) + ) { + return config.buildSettings[propertyName]; + } + } + + return null; + } + + /** + * Check if a path should be excluded based on glob patterns + */ + private shouldExclude(filePath: string, excludePatterns: string[]): boolean { + for (const pattern of excludePatterns) { + const matcher = new Minimatch(pattern, { dot: true }); + if (matcher.match(filePath)) { + return true; + } + } + return false; + } + + /** + * Apply SPM packages to watch app targets + */ + private async applySPMPackagesToTargets( + targetNames: string[], + platformData: IPlatformData, + basedir: string, + watchSPMPackages: any[], + ): Promise { + try { + this.$logger.debug( + `applySPMPackagesToTargets ${JSON.stringify(watchSPMPackages)}`, + ); + if (watchSPMPackages.length === 0) { + return; + } + + this.$logger.debug( + `Applying ${watchSPMPackages.length} SPM package(s) to targets:${targetNames}`, + ); + + // Add SPM packages to each watch target + const assignments: IosSPMPackageAssignment[] = []; + for (const pkg of watchSPMPackages) { + if ("path" in pkg) { + pkg.path = path.resolve(basedir, pkg.path); + } + + this.$logger.debug( + `Adding SPM package ${JSON.stringify(pkg)} to targets ${targetNames}`, + ); + for (const targetName of targetNames) { + assignments.push({ targetName, package: pkg }); + } + } + + if ( + !this.$spmPbxprojService.addPackages( + platformData.projectRoot, + assignments, + ) + ) { + this.$logger.debug( + `No SPM packages were applied to targets ${targetNames}`, + ); + return; + } + + this.$logger.debug( + `Successfully applied SPM packages to targets ${targetNames}`, + ); + } catch (err) { + this.$logger.debug( + `Error applying SPM packages to targets ${targetNames} "`, + err, + ); + } + } + + /** + * Get SPM packages configured for watch app targets + */ + private getWatchSPMPackages(platformData: IPlatformData): IosSPMPackage[] { + const $projectConfigService = injector.resolve("projectConfigService"); + + // Check for watch-specific SPM packages in config + const watchPackages = $projectConfigService.getValue( + `${platformData.platformNameLowerCase}.watchApp.SPMPackages`, + [], + ); + + return watchPackages; } } diff --git a/lib/services/ios/spm-pbxproj-service.ts b/lib/services/ios/spm-pbxproj-service.ts new file mode 100644 index 0000000000..0ab41f868c --- /dev/null +++ b/lib/services/ios/spm-pbxproj-service.ts @@ -0,0 +1,415 @@ +import * as path from "path"; +import * as semver from "semver"; +import { injector } from "../../common/yok"; +import { IFileSystem } from "../../common/declarations"; + +/** + * Writes Swift Package references directly into an Xcode project's pbxproj. + * + * Adding a Swift package to a target means touching four places in the + * pbxproj, which is why this is not a one-liner: + * + * 1. an `XCRemoteSwiftPackageReference` / `XCLocalSwiftPackageReference` + * object describing *where* the package comes from, listed in the + * project's `packageReferences`; + * 2. an `XCSwiftPackageProductDependency` per linked product (lib); + * 3. a `PBXBuildFile` wrapping each product dependency; + * 4. an entry in the target's Frameworks build phase, plus the target's + * `packageProductDependencies`. + * + * Every entry is keyed by its pbxproj comment (e.g. `XCRemoteSwiftPackageReference + * "Auth0"`), and an existing entry is updated in place rather than duplicated — + * so applying the same set of packages repeatedly (which the CLI does on every + * prepare) is idempotent and doesn't grow the pbxproj. + */ +export class SPMPbxprojService implements ISPMPbxprojService { + constructor( + private $fs: IFileSystem, + private $logger: ILogger, + private $xcode: IXcode, + ) {} + + /** + * Adds each package to its target in a single parse/write cycle. + * + * Returns true when the pbxproj was written. Missing targets are warned + * about and skipped rather than failing the whole batch — a package meant + * for a widget target shouldn't stop the app's own packages from applying. + */ + public addPackages( + projectRoot: string, + assignments: IosSPMPackageAssignment[], + ): boolean { + if (!assignments.length) { + return false; + } + + const pbxProjPath = this.findPbxProjPath(projectRoot); + if (!pbxProjPath) { + this.$logger.trace( + `SPM: no Xcode project found under ${projectRoot}; skipping.`, + ); + return false; + } + + const project = new this.$xcode.project(pbxProjPath); + project.parseSync(); + + let added = false; + for (const { targetName, package: pkg } of assignments) { + const targetId = this.findTargetId(project, targetName); + if (!targetId) { + this.$logger.warn( + `SPM: target "${targetName}" not found in ${path.basename(pbxProjPath)} — skipping package "${pkg.name}".`, + ); + continue; + } + + if (this.addPackageToTarget(project, targetId, pkg, projectRoot)) { + added = true; + } + } + + if (!added) { + return false; + } + + this.$fs.writeFile( + pbxProjPath, + project.writeSync({ omitEmptyValues: true }), + ); + return true; + } + + /** + * Locates `.xcodeproj/project.pbxproj` under the platform project + * root. The project is named after the app, so it's discovered rather than + * assumed. + */ + private findPbxProjPath(projectRoot: string): string | null { + if (!this.$fs.exists(projectRoot)) { + return null; + } + + const xcodeprojName = this.$fs + .readDirectory(projectRoot) + .find((entry) => entry.endsWith(".xcodeproj")); + if (!xcodeprojName) { + return null; + } + + const pbxProjPath = path.join( + projectRoot, + xcodeprojName, + "project.pbxproj", + ); + return this.$fs.exists(pbxProjPath) ? pbxProjPath : null; + } + + /** + * Resolves a target name to its pbxproj uuid. Target names in the pbxproj + * are quoted when they contain spaces, so both forms are matched. + */ + private findTargetId(project: any, targetName: string): string | null { + const targets = project.pbxNativeTargetSection() ?? {}; + for (const key of Object.keys(targets)) { + if (key.endsWith("_comment")) { + continue; + } + const name = targets[key]?.name; + if (name === targetName || name === `"${targetName}"`) { + return key; + } + } + return null; + } + + /** Returns true when the package was actually linked into the target. */ + private addPackageToTarget( + project: any, + targetId: string, + pkg: IosSPMPackage, + projectRoot: string, + ): boolean { + const target = project.pbxNativeTargetSection()[targetId]; + + // A target without a Frameworks build phase has nowhere to link the + // products; adding the package reference alone would leave the project + // in a state Xcode reports as corrupt, so bail out loudly instead — + // before touching the project, so a skipped package leaves no trace. + // (Resolved from the target's own buildPhases: the xcode lib's + // pbxFrameworksBuildPhaseObj falls back to *any* target's Frameworks + // phase when this one has none, which would link into the wrong target.) + const frameworkBuildPhaseObj = this.findFrameworksBuildPhase( + project, + target, + ); + if (!frameworkBuildPhaseObj) { + this.$logger.warn( + `SPM: target for package "${pkg.name}" has no Frameworks build phase — skipping.`, + ); + return false; + } + + const firstProject = project.getFirstProject().firstProject; + const packageReferences: any[] = (firstProject["packageReferences"] ??= []); + const packageProductReferences: any[] = (target[ + "packageProductDependencies" + ] ??= []); + const frameworkBuildPhaseFiles: any[] = (frameworkBuildPhaseObj["files"] ??= + []); + + let packageReferenceComment: string; + let packageReferenceSection: string; + let packageReferenceSectionContent: Record; + + if ("path" in pkg) { + // local package — Xcode stores the location relative to the project + const relativePath = path.relative( + projectRoot, + path.resolve(projectRoot, pkg.path), + ); + packageReferenceComment = `XCLocalSwiftPackageReference "${relativePath}"`; + packageReferenceSection = "XCLocalSwiftPackageReference"; + packageReferenceSectionContent = { + isa: packageReferenceSection, + relativePath: JSON.stringify(relativePath), + }; + } else { + packageReferenceComment = `XCRemoteSwiftPackageReference "${pkg.name}"`; + packageReferenceSection = "XCRemoteSwiftPackageReference"; + packageReferenceSectionContent = { + isa: packageReferenceSection, + repositoryURL: JSON.stringify(pkg.repositoryURL), + requirement: quoteValuesForPbxproj(classifyVersion(pkg.version)), + }; + } + + const { + uuid: spmPackageReferenceUUID, + comment: spmPackageReferenceComment, + } = this.addOrUpdateEntry( + project, + packageReferenceSection, + packageReferenceComment, + packageReferenceSectionContent, + ); + + this.addOrUpdateArrayEntry(packageReferences, spmPackageReferenceUUID, { + value: spmPackageReferenceUUID, + comment: packageReferenceComment, + }); + + for (const lib of pkg.libs ?? []) { + // The comment is just the product name, which two different packages + // can share (e.g. both exposing a "Core" lib) — so entries here are + // additionally matched on the package they belong to, otherwise the + // second package would silently repoint the first one's entries. + const { uuid: spmProductDependencyUUID } = this.addOrUpdateEntry( + project, + "XCSwiftPackageProductDependency", + lib, + { + isa: "XCSwiftPackageProductDependency", + package: spmPackageReferenceUUID, + package_comment: spmPackageReferenceComment, + productName: lib, + }, + (existing) => existing.package === spmPackageReferenceUUID, + ); + + const libComment = `${lib} in Frameworks`; + + const { uuid: spmBuildFileUuid } = this.addOrUpdateEntry( + project, + "PBXBuildFile", + libComment, + { + isa: "PBXBuildFile", + productRef: spmProductDependencyUUID, + productRef_comment: lib, + }, + (existing) => existing.productRef === spmProductDependencyUUID, + ); + + this.addOrUpdateArrayEntry( + packageProductReferences, + spmProductDependencyUUID, + { + value: spmProductDependencyUUID, + comment: lib, + }, + ); + + this.addOrUpdateArrayEntry(frameworkBuildPhaseFiles, spmBuildFileUuid, { + value: spmBuildFileUuid, + comment: libComment, + }); + } + + return true; + } + + /** Finds the Frameworks build phase listed in this target's own buildPhases. */ + private findFrameworksBuildPhase(project: any, target: any): any | null { + const section = + project.hash.project.objects["PBXFrameworksBuildPhase"] ?? {}; + for (const phase of target.buildPhases ?? []) { + const phaseObj = section[phase.value]; + if (phaseObj) { + return phaseObj; + } + } + return null; + } + + /** Replaces a matching array entry in place, or appends it. */ + private addOrUpdateArrayEntry( + array: any[], + lookupValue: string, + value: any, + ): void { + const existing = array.find((entry) => entry.value === lookupValue); + if (existing) { + Object.assign(existing, value); + return; + } + array.push(value); + } + + /** + * Writes an object into a pbxproj section, reusing the uuid of an entry + * with the same comment when one is already present. The comment is the + * identity of an entry here — it's what keeps repeated applies idempotent. + * When the comment alone is ambiguous (product names are not unique across + * packages), `matches` narrows the lookup to the right entry. + */ + private addOrUpdateEntry( + project: any, + section: string, + entryComment: string, + entry: any, + matches?: (existing: any) => boolean, + ): { uuid: string; comment: string } { + const pbxSection = (project.hash.project.objects[section] ??= {}); + const entryUuid = + this.findUuidByComment(project, section, entryComment, matches) ?? + project.generateUuid(); + + pbxSection[`${entryUuid}_comment`] = entryComment; + pbxSection[entryUuid] = entry; + + return { uuid: entryUuid, comment: entryComment }; + } + + private findUuidByComment( + project: any, + section: string, + comment: string, + matches?: (existing: any) => boolean, + ): string | null { + const pbxSection = project.hash.project.objects[section] ?? {}; + const commentKey = Object.keys(pbxSection).find((key) => { + if (!key.endsWith("_comment") || pbxSection[key] !== comment) { + return false; + } + if (!matches) { + return true; + } + const existing = pbxSection[key.replace(/_comment$/, "")]; + return existing != null && matches(existing); + }); + return commentKey ? commentKey.replace(/_comment$/, "") : null; + } +} + +/** + * Maps a package version string to the `requirement` object Xcode expects: + * + * "1.0.0" -> { kind: exactVersion, version } + * "^1.0.0" -> { kind: upToNextMajorVersion, minimumVersion } + * "~1.0.0" -> { kind: upToNextMinorVersion, minimumVersion } + * ">=1.0.0 <2.0.0" -> { kind: versionRange, minimumVersion, maximumVersion } + * "#" -> { kind: revision, revision } + * anything else -> { kind: branch, branch } + * + * A non-semver value is treated as a branch name, which is how a package can + * be pinned to e.g. "main". + */ +export function classifyVersion(version: string): Record { + if (version.startsWith("#")) { + return { + kind: "revision", + revision: version.replace("#", ""), + }; + } + + if (semver.valid(version)) { + return { + kind: "exactVersion", + version, + }; + } + + const range = semver.validRange(version); + if (range) { + const minimumVersion = semver.minVersion(range)?.version; + if (version.startsWith("^")) { + return { + kind: "upToNextMajorVersion", + minimumVersion, + }; + } + if (version.startsWith("~")) { + return { + kind: "upToNextMinorVersion", + minimumVersion, + }; + } + + const maximumVersion = semver.coerce( + version.replace(minimumVersion ?? "", ""), + )?.version; + + if (maximumVersion && maximumVersion !== minimumVersion) { + return { + kind: "versionRange", + minimumVersion, + maximumVersion, + }; + } + + return { + kind: "upToNextMajorVersion", + minimumVersion, + }; + } + + return { + kind: "branch", + branch: version, + }; +} + +/** + * The charset Xcode itself leaves unquoted in a pbxproj. The pbxproj writer + * emits values verbatim, so anything outside it — a prerelease version like + * "1.0.0-beta.1", a branch like "release 1.0" — must be quoted by the caller + * or the written file is malformed. + */ +const UNQUOTED_PBX_VALUE = /^[A-Za-z0-9_$./]+$/; + +function quoteValuesForPbxproj( + obj: Record, +): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = + typeof value === "string" && !UNQUOTED_PBX_VALUE.test(value) + ? JSON.stringify(value) + : value; + } + return result; +} + +injector.register("spmPbxprojService", SPMPbxprojService); diff --git a/lib/services/ios/spm-service.ts b/lib/services/ios/spm-service.ts index c7462b9d8e..b4515b4dc7 100644 --- a/lib/services/ios/spm-service.ts +++ b/lib/services/ios/spm-service.ts @@ -1,15 +1,41 @@ import { injector } from "../../common/yok"; import { IProjectConfigService, IProjectData } from "../../definitions/project"; -import { MobileProject } from "@nstudio/trapezedev-project"; import { IPlatformData } from "../../definitions/platform"; +import { IFileSystem } from "../../common/declarations"; +import { ITerminalSpinnerService } from "../../definitions/terminal-spinner-service"; +import { color } from "../../color"; import path = require("path"); +import os = require("os"); +import fs = require("fs"); export class SPMService implements ISPMService { + // SwiftPM keeps one bare clone per source package here (shared by Xcode and + // xcodebuild). Watching a package's clone grow is the only visibility into + // an otherwise silent long fetch — SwiftPM clones a repository's ENTIRE git + // history to read Package.swift, so a package hosted in a multi-GB repo can + // legitimately "fetch" for tens of minutes with no output. + private static readonly SWIFTPM_REPO_CACHE = path.join( + os.homedir(), + "Library", + "Caches", + "org.swift.swiftpm", + "repositories", + ); + // Once a single package's clone passes this size, surface a one-time note + // explaining why the fetch is slow (250 MB is already far beyond any + // reasonably-hosted Swift package). + private static readonly LARGE_CLONE_NOTE_BYTES = 250 * 1024 * 1024; + // Lines of raw xcodebuild output kept for the failure report. + private static readonly OUTPUT_TAIL_LINES = 25; + constructor( private $logger: ILogger, + private $fs: IFileSystem, private $projectConfigService: IProjectConfigService, + private $terminalSpinnerService: ITerminalSpinnerService, private $xcodebuildCommandService: IXcodebuildCommandService, private $xcodebuildArgsService: IXcodebuildArgsService, + private $spmPbxprojService: ISPMPbxprojService, ) {} public getSPMPackages( @@ -36,12 +62,23 @@ export class SPMService implements ISPMService { ): void { // include swift packages from plugin configs // but allow app packages to override plugin packages with the same name - const appPackageNames = new Set(appPackages.map(pkg => pkg.name)); - + const appPackageNames = new Set(appPackages.map((pkg) => pkg.name)); + // multiple plugins may declare the same package (e.g. a shared shim) — + // only the first declaration is added; a second same-name package would + // produce duplicate (and possibly conflicting) references in the pbxproj. + const addedPluginPackageNames = new Set(); + for (const pluginPkg of pluginPackages) { if (appPackageNames.has(pluginPkg.name)) { - this.$logger.trace(`SPM: app package overrides plugin package: ${pluginPkg.name}`); + this.$logger.trace( + `SPM: app package overrides plugin package: ${pluginPkg.name}`, + ); + } else if (addedPluginPackageNames.has(pluginPkg.name)) { + this.$logger.trace( + `SPM: skipping duplicate plugin package: ${pluginPkg.name}`, + ); } else { + addedPluginPackageNames.add(pluginPkg.name); appPackages.push(pluginPkg); } } @@ -72,63 +109,454 @@ export class SPMService implements ISPMService { return; } - const project = new MobileProject(platformData.projectRoot, { - ios: { - path: ".", - }, - enableAndroid: false, - }); - await project.load(); - - // note: in trapeze both visionOS and iOS are handled by the ios project. - if (!project.ios) { - this.$logger.trace("SPM: no iOS project found via trapeze."); - return; - } + // name every package and where it comes from — when resolution is + // slow or fails, this is the first thing needed to tell WHICH + // dependency is responsible. + this.$logger.info(this.formatPackageListing(spmPackages)); // todo: handle removing packages? Or just warn and require a clean? + const assignments: IosSPMPackageAssignment[] = []; for (const pkg of spmPackages) { if ("path" in pkg) { // resolve the path relative to the project root this.$logger.trace("SPM: resolving path for package: ", pkg.path); pkg.path = path.resolve(projectData.projectDir, pkg.path); + if (!this.$fs.exists(pkg.path)) { + // surface this now — otherwise the only symptom is a cryptic + // xcodebuild resolution failure much later. + this.$logger.warn( + `SPM: local package path for "${pkg.name}" does not exist: ${pkg.path} — Xcode will fail to resolve it.`, + ); + } } this.$logger.trace(`SPM: adding package ${pkg.name} to project.`, pkg); - await project.ios.addSPMPackage(projectData.projectName, pkg); + assignments.push({ targetName: projectData.projectName, package: pkg }); // Add to other Targets if specified (like widgets, etc.) - if (pkg.targets?.length) { - for (const target of pkg.targets) { - await project.ios.addSPMPackage(target, pkg); - } + for (const target of pkg.targets ?? []) { + assignments.push({ targetName: target, package: pkg }); } } - await project.commit(); + + // note: visionOS shares the iOS Xcode project, so the same pbxproj is + // edited for both platforms. + if ( + !this.$spmPbxprojService.addPackages( + platformData.projectRoot, + assignments, + ) + ) { + this.$logger.trace("SPM: no packages were applied to the project."); + return; + } // finally resolve the dependencies - await this.resolveSPMDependencies(platformData, projectData); + await this.resolveSPMDependencies(platformData, projectData, { + showProgress: true, + }); } catch (err) { + // best-effort, but don't bury the failure below trace level — a red + // resolve spinner with no visible reason is confusing. Warn with the + // message, keep the full error at trace. + this.$logger.warn( + `Failed to apply Swift Package dependencies: ${err?.message ?? err}`, + ); this.$logger.trace("SPM: error applying SPM packages: ", err); } } + /** + * Resolves (downloads + pins) the Swift Package dependencies referenced by + * the Xcode project. On a first build this is where the NativeScript runtime + * (now distributed as a remote Swift package) and any other SPM packages are + * actually downloaded — which can take a while. Pass `showProgress` to render + * a live spinner so the CLI doesn't look stalled while that happens. + * + * In verbose mode (`--log trace`) the condensed spinner is bypassed and + * xcodebuild's raw resolution log is streamed straight through instead. + */ public async resolveSPMDependencies( platformData: IPlatformData, projectData: IProjectData, + options?: { showProgress?: boolean }, ) { - await this.$xcodebuildCommandService.executeCommand( - this.$xcodebuildArgsService - .getXcodeProjectArgs(platformData, projectData) - .concat([ - "-destination", - "generic/platform=iOS", - "-resolvePackageDependencies", - ]), - { + const args = this.$xcodebuildArgsService + .getXcodeProjectArgs(platformData, projectData) + .concat([ + "-destination", + "generic/platform=iOS", + "-resolvePackageDependencies", + ]); + + // Without progress, or when verbose: let xcodebuild's own resolution log + // stream straight to the terminal (inherited stdio). Verbose users want + // the raw log, not a condensed spinner that hides it. + if (!options?.showProgress || this.$logger.isVerbose()) { + await this.$xcodebuildCommandService.executeCommand(args, { + cwd: projectData.projectDir, + message: "Resolving Swift Package dependencies...", + }); + return; + } + + const spinner = this.$terminalSpinnerService.createSpinner(); + const startedAt = Date.now(); + let activity = "Resolving Swift Package dependencies"; + let lineBuffer = ""; + // package currently being git-fetched (short name), if any — used to + // measure its growing clone in the SwiftPM cache so a long silent fetch + // shows visible progress ("2.31 GB of git history fetched") instead of + // looking hung. + let fetchingPackageRef: string = null; + let fetchedBytes = 0; + let largeCloneNoted = false; + // rolling tail of raw xcodebuild output for the failure report. + const outputTail: string[] = []; + + const render = () => { + const elapsed = Math.round((Date.now() - startedAt) / 1000); + const fetched = + fetchedBytes > 0 + ? ` — ${this.formatBytes(fetchedBytes)} of git history fetched` + : ""; + spinner.text = `${activity}…${fetched} ${color.dim(`(${this.formatElapsed(elapsed)})`)}`; + }; + // keep the elapsed timer ticking even when xcodebuild is silent (e.g. + // while a repository clones or a binary artifact downloads) so the user + // can see it's alive. Every 5th tick, measure the in-progress clone. + let tickCount = 0; + const ticker = setInterval(() => { + tickCount++; + if (fetchingPackageRef && tickCount % 5 === 0) { + fetchedBytes = this.getPackageCloneSizeBytes(fetchingPackageRef); + if ( + !largeCloneNoted && + fetchedBytes >= SPMService.LARGE_CLONE_NOTE_BYTES + ) { + largeCloneNoted = true; + // persist a one-time explanation above the spinner: this is the + // point where users otherwise assume the CLI is stuck. + spinner.stopAndPersist({ + symbol: color.yellow("ℹ"), + text: color.yellow( + `the "${fetchingPackageRef}" package is hosted in a repository with a large git history — ` + + `SwiftPM clones the entire repository on first fetch, which can take a long time.${os.EOL}` + + ` cache: ${SPMService.SWIFTPM_REPO_CACHE}`, + ), + }); + spinner.start(); + } + } + render(); + }, 1000); + + const onProgress = (chunk: { data: string; pipe: string }) => { + lineBuffer += chunk.data; + // tolerate CRLF as well as LF so parsed lines never carry a stray \r + const lines = lineBuffer.split(/\r?\n/); + // keep the last (possibly partial) line in the buffer + lineBuffer = lines.pop(); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed) { + this.$logger.trace(`SPM: ${trimmed}`); + outputTail.push(trimmed); + if (outputTail.length > SPMService.OUTPUT_TAIL_LINES) { + outputTail.shift(); + } + } + const described = this.describeSPMActivity(line); + if (described) { + activity = described; + // track which package a "Fetching " line refers to so the + // ticker can measure its clone; any other activity means the + // fetch finished. + if (/^Fetching\b/i.test(trimmed)) { + fetchingPackageRef = this.shortenPackageRef(trimmed); + } else { + fetchingPackageRef = null; + fetchedBytes = 0; + } + render(); + } + } + }; + + render(); + spinner.start(); + try { + await this.$xcodebuildCommandService.executeCommand(args, { cwd: projectData.projectDir, - message: "Resolving SPM dependencies...", - }, + onProgress, + }); + const elapsed = Math.round((Date.now() - startedAt) / 1000); + spinner.succeed( + color.green("Swift Package dependencies resolved") + + color.dim(` (${this.formatElapsed(elapsed)})`), + ); + } catch (err) { + spinner.fail(color.red("Failed to resolve Swift Package dependencies")); + // the spinner swallowed the raw log — replay the tail so the actual + // xcodebuild error is visible without rerunning in verbose mode. + if (outputTail.length) { + this.$logger.info(color.dim("xcodebuild output (last lines):")); + for (const line of outputTail) { + this.$logger.info(color.dim(` ${line}`)); + } + } + throw err; + } finally { + clearInterval(ticker); + } + } + + /** + * Best-effort pre-resolve before a build so the (potentially slow) first-time + * Swift package download happens under a clear progress indicator instead of + * silently inside the subsequent "Xcode build..." step. No-op when the + * project has no SPM references or they're already resolved. + */ + public async ensureSPMDependenciesResolved( + platformData: IPlatformData, + projectData: IProjectData, + ) { + if (!this.hasSPMReferences(platformData, projectData)) { + this.$logger.trace("SPM: project has no Swift Package references."); + return; + } + + if (this.arePackagesResolved(platformData, projectData)) { + this.$logger.trace( + "SPM: Swift Package dependencies already resolved; skipping pre-resolve.", + ); + return; + } + + try { + await this.resolveSPMDependencies(platformData, projectData, { + showProgress: true, + }); + } catch (err) { + // non-fatal: the build itself will resolve packages and surface the + // authoritative error if something is genuinely wrong. + this.$logger.trace("SPM: pre-resolve failed (continuing): ", err); + } + } + + /** + * Maps a raw xcodebuild/SwiftPM resolution log line to a concise, + * user-facing activity description (or null for lines we don't surface). + */ + private describeSPMActivity(line: string): string | null { + const trimmed = line.trim(); + if (!trimmed) { + return null; + } + + // the big, otherwise-silent wait on a first build: a binary artifact + // (the NativeScript runtime xcframework) downloading. + if (/Downloading binary artifact/i.test(trimmed)) { + if (/ios-spm|nativescript/i.test(trimmed)) { + return "Downloading the NativeScript runtime (first build only)"; + } + return "Downloading Swift Package binaries (first build only)"; + } + if (/^Fetching\b/i.test(trimmed)) { + return this.describePackageActivity("Fetching", trimmed); + } + if (/^Cloning\b/i.test(trimmed)) { + return this.describePackageActivity("Cloning", trimmed); + } + if (/Computing version for/i.test(trimmed)) { + return "Computing package versions"; + } + if (/Resolve Package Graph/i.test(trimmed)) { + return "Resolving Swift Package graph"; + } + if (/Resolved source packages/i.test(trimmed)) { + return "Finalizing Swift Package dependencies"; + } + return null; + } + + /** + * Builds a stable, self-explanatory activity label with the package being + * worked on in parentheses, e.g. "Fetching Swift Packages (Auth0.swift)". + */ + private describePackageActivity(verb: string, line: string): string { + const packageRef = this.shortenPackageRef(line); + return packageRef + ? `${verb} Swift Packages (${packageRef})` + : `${verb} Swift Packages`; + } + + /** + * Extracts a short, readable name from a SwiftPM repo URL/log line, or null + * when the line contains no URL to name the package by. + */ + private shortenPackageRef(line: string): string | null { + const match = line.match(/https?:\/\/\S+/); + if (!match) { + return null; + } + return path + .basename(match[0]) + .replace(/\.git$/, "") + .replace(/[)\s].*$/, ""); + } + + /** Formats an elapsed duration in whole seconds as "5m 15s". */ + private formatElapsed(totalSeconds: number): string { + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}m ${seconds}s`; + } + + /** + * Multi-line listing of every package and its source — one package per + * line, separated by the platform EOL so entries never clump together in + * terminal output on macOS, Windows, or Linux. + */ + private formatPackageListing(spmPackages: IosSPMPackage[]): string { + return [ + "Swift Packages:", + ...spmPackages.map((pkg) => ` ${this.describePackageSource(pkg)}`), + ].join(os.EOL); + } + + /** + * One-line description of a package and where it resolves from, e.g. + * "FontManager (1.0.12 · https://github.com/NativeScript/font-manager.git)" + * or "CanvasNative (local: node_modules/@nativescript/canvas/platforms/ios/NativeScriptV8)". + */ + private describePackageSource(pkg: IosSPMPackage): string { + if ("path" in pkg) { + return `${pkg.name} (local: ${pkg.path})`; + } + return `${pkg.name} (${pkg.version} · ${pkg.repositoryURL})`; + } + + /** + * Size on disk of the SwiftPM cache clone(s) for a package ref (the short + * name produced by shortenPackageRef). Cache entries are named + * "-". Returns 0 when nothing is there (yet). + */ + private getPackageCloneSizeBytes(packageRef: string): number { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(SPMService.SWIFTPM_REPO_CACHE, { + withFileTypes: true, + }); + } catch (err) { + return 0; + } + let total = 0; + for (const entry of entries) { + if ( + entry.isDirectory() && + (entry.name === packageRef || entry.name.startsWith(`${packageRef}-`)) + ) { + total += this.getDirectorySizeBytes( + path.join(SPMService.SWIFTPM_REPO_CACHE, entry.name), + ); + } + } + return total; + } + + /** + * Recursive directory size via the raw fs API. Tolerates files vanishing + * mid-walk (git renames its temp packfiles while cloning) and does not + * follow symlinks. + */ + private getDirectorySizeBytes(dirPath: string): number { + let total = 0; + const pending = [dirPath]; + while (pending.length) { + const current = pending.pop(); + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch (err) { + continue; + } + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + try { + if (entry.isDirectory()) { + pending.push(fullPath); + } else if (entry.isFile()) { + total += fs.statSync(fullPath).size; + } + } catch (err) { + // entry disappeared between readdir and stat — ignore + } + } + } + return total; + } + + /** Formats a byte count as a short human-readable size ("2.31 GB"). */ + private formatBytes(bytes: number): string { + const GB = 1024 ** 3; + const MB = 1024 ** 2; + if (bytes >= GB) { + return `${(bytes / GB).toFixed(2)} GB`; + } + if (bytes >= MB) { + return `${Math.round(bytes / MB)} MB`; + } + return `${Math.round(bytes / 1024)} KB`; + } + + /** True when the Xcode project references any Swift packages. */ + private hasSPMReferences( + platformData: IPlatformData, + projectData: IProjectData, + ): boolean { + const pbxprojPath = path.join( + platformData.projectRoot, + `${projectData.projectName}.xcodeproj`, + "project.pbxproj", ); + if (!this.$fs.exists(pbxprojPath)) { + return false; + } + const contents = this.$fs.readText(pbxprojPath); + return ( + contents.includes("XCRemoteSwiftPackageReference") || + contents.includes("XCLocalSwiftPackageReference") || + contents.includes("packageReferences") + ); + } + + /** + * True when a Package.resolved already exists for the project — i.e. packages + * have been resolved at least once, so the build's own resolve step will be a + * fast no-op rather than a slow, silent first-time download. + */ + private arePackagesResolved( + platformData: IPlatformData, + projectData: IProjectData, + ): boolean { + const candidates = [ + path.join( + platformData.projectRoot, + `${projectData.projectName}.xcworkspace`, + "xcshareddata", + "swiftpm", + "Package.resolved", + ), + path.join( + platformData.projectRoot, + `${projectData.projectName}.xcodeproj`, + "project.xcworkspace", + "xcshareddata", + "swiftpm", + "Package.resolved", + ), + ]; + return candidates.some((p) => this.$fs.exists(p)); } } injector.register("spmService", SPMService); diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts index 8eee2ea366..fbc5a2ebe4 100644 --- a/lib/services/ios/xcodebuild-args-service.ts +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -167,13 +167,32 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { // Introduced in Xcode 14+ // ref: https://forums.swift.org/t/telling-xcode-14-beta-4-to-trust-build-tool-plugins-programatically/59305/5 const skipPackageValidation = "-skipPackagePluginValidation"; - + // Introduced in Xcode 15+ to trust Swift macros (compiler plugins) + // non-interactively. Required for SPM packages that ship macros + // (e.g. apple/RealityKitScripting), otherwise the build fails with: + // "Macro '...' from package '...' must be enabled before it can be used" + // ref: https://developer.apple.com/documentation/xcode/writing-swift-macros + const skipMacroValidation = "-skipMacroValidation"; const extraArgs: string[] = [ "-scheme", projectData.projectName, skipPackageValidation, + skipMacroValidation, ]; + // Selects how xcodebuild authenticates against private Swift Package + // registries ("netrc" or "keychain"). It describes the machine's + // credential setup rather than the build, so it is read from the + // environment instead of a command-line option. + const packageAuthorizationProvider = + process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER; + if (packageAuthorizationProvider) { + extraArgs.push( + "-packageAuthorizationProvider", + packageAuthorizationProvider, + ); + } + const BUILD_SETTINGS_FILE_PATH = path.join( projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName, @@ -187,6 +206,22 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { // references: https://medium.com/@iostechset/why-cocoapods-eats-app-icons-79fe729808d4 // https://github.com/CocoaPods/CocoaPods/issues/7003 + // Xcode 26 makes Swift "explicitly built modules" the default. A + // regression there prevents macro/compiler-plugin SPM targets from + // resolving their swift-syntax module dependencies, failing with: + // "Unable to resolve module dependency: 'SwiftSyntax'" (and SwiftParser, + // SwiftSyntaxMacros, SwiftCompilerPlugin, SwiftDiagnostics). + // Passed as a command-line build setting so it overrides ALL targets, + // including the package targets we don't control. + // ref: https://forums.swift.org/t/xcode-26-unable-to-find-module-dependency/80516 + const explicitModulesProperty = "SWIFT_ENABLE_EXPLICIT_MODULES"; + const explicitModulesValue = + this.$xcconfigService.readPropertyValue( + BUILD_SETTINGS_FILE_PATH, + explicitModulesProperty, + ) || "NO"; + extraArgs.push(`${explicitModulesProperty}=${explicitModulesValue}`); + const deployTargetProperty = "IPHONEOS_DEPLOYMENT_TARGET"; const deployTargetVersion = this.$xcconfigService.readPropertyValue( BUILD_SETTINGS_FILE_PATH, @@ -205,6 +240,18 @@ export class XcodebuildArgsService implements IXcodebuildArgsService { extraArgs.push(`${swiftUIBootProperty}=${swiftUIBootValue}`); } + // Swift macro/compiler-plugin SPM targets must be code-signed with a + // development team when building for a device. Pass DEVELOPMENT_TEAM as a + // command-line build setting so it applies to SPM package targets too. + const developmentTeamProperty = "DEVELOPMENT_TEAM"; + const developmentTeamValue = this.$xcconfigService.readPropertyValue( + BUILD_SETTINGS_FILE_PATH, + developmentTeamProperty, + ); + if (developmentTeamValue) { + extraArgs.push(`${developmentTeamProperty}=${developmentTeamValue}`); + } + if (this.$fs.exists(xcworkspacePath)) { return ["-workspace", xcworkspacePath, ...extraArgs]; } diff --git a/lib/services/ios/xcodebuild-command-service.ts b/lib/services/ios/xcodebuild-command-service.ts index f5a1f7a6af..4b1b994295 100644 --- a/lib/services/ios/xcodebuild-command-service.ts +++ b/lib/services/ios/xcodebuild-command-service.ts @@ -10,22 +10,46 @@ export class XcodebuildCommandService implements IXcodebuildCommandService { constructor( private $childProcess: IChildProcess, private $errors: IErrors, - private $logger: ILogger + private $logger: ILogger, ) {} public async executeCommand( args: string[], options: { cwd: string; - stdio: string; + stdio?: string; message?: string; spawnOptions?: any; - } + // When provided, xcodebuild's output is piped (rather than inherited) + // and forwarded here line-by-line so the caller can render its own + // progress UI (e.g. a spinner for SPM resolution/download activity). + onProgress?: (chunk: { data: string; pipe: string }) => void; + }, ): Promise { - const { message, cwd, stdio, spawnOptions } = options; - this.$logger.info(message || "Xcode build..."); + const { message, cwd, stdio, spawnOptions, onProgress } = options; + + // A caller rendering its own progress UI owns stdout, so skip the + // default "Xcode build..." line that would otherwise clobber it. + if (!onProgress) { + this.$logger.info(message || "Xcode build..."); + } - const childProcessOptions = { cwd, stdio: stdio || "inherit" }; + const childProcessOptions = { + cwd, + stdio: onProgress ? "pipe" : stdio || "inherit", + }; + + let detachProgress: () => void; + if (onProgress) { + const handler = (chunk: { data: string; pipe: string }) => + onProgress(chunk); + this.$childProcess.on(constants.BUILD_OUTPUT_EVENT_NAME, handler); + detachProgress = () => + this.$childProcess.removeListener( + constants.BUILD_OUTPUT_EVENT_NAME, + handler, + ); + } try { const commandResult = await this.$childProcess.spawnFromEvent( @@ -36,12 +60,14 @@ export class XcodebuildCommandService implements IXcodebuildCommandService { spawnOptions || { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true, - } + }, ); return commandResult; } catch (err) { this.$errors.fail(err.message); + } finally { + detachProgress?.(); } } } diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts index 2ead95e8ac..cedcf2b5c2 100644 --- a/lib/services/livesync/ios-livesync-service.ts +++ b/lib/services/livesync/ios-livesync-service.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { IOSDeviceLiveSyncService } from "./ios-device-livesync-service"; import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; import { APP_FOLDER_NAME } from "../../constants"; +import { LiveSyncPaths } from "../../common/constants"; import { performanceLog } from "../../common/decorators"; import { IPlatformsDataService } from "../../definitions/platform"; import { @@ -27,7 +28,7 @@ export class IOSLiveSyncService private $tempService: ITempService, $devicePathProvider: IDevicePathProvider, $logger: ILogger, - $options: IOptions + $options: IOptions, ) { super( $fs, @@ -35,7 +36,7 @@ export class IOSLiveSyncService $platformsDataService, $projectFilesManager, $devicePathProvider, - $options + $options, ); } @@ -49,12 +50,12 @@ export class IOSLiveSyncService const projectData = syncInfo.projectData; const platformData = this.$platformsDataService.getPlatformData( device.deviceInfo.platform, - projectData + projectData, ); const deviceAppData = await this.getAppData(syncInfo); const projectFilesPath = path.join( platformData.appDestinationDirectoryPath, - APP_FOLDER_NAME + APP_FOLDER_NAME, ); const tempZip = await this.$tempService.path({ @@ -70,17 +71,155 @@ export class IOSLiveSyncService return path.join(APP_FOLDER_NAME, path.relative(projectFilesPath, res)); }); - await device.fileSystem.transferFiles(deviceAppData, [ - { - getLocalPath: () => tempZip, - getDevicePath: () => deviceAppData.deviceSyncZipPath, - getRelativeToProjectBasePath: () => "../sync.zip", - deviceProjectRootPath: await deviceAppData.getDeviceProjectRootPath(), - }, - ]); + const deviceProjectRootPath = + await deviceAppData.getDeviceProjectRootPath(); + const transferSyncZip = () => + device.fileSystem.transferFiles(deviceAppData, [ + { + getLocalPath: () => tempZip, + getDevicePath: () => deviceAppData.deviceSyncZipPath, + getRelativeToProjectBasePath: () => "../sync.zip", + deviceProjectRootPath, + }, + ]); + + // ── Fail-closed delivery verification ────────────────────────── + // + // The AFC transfer has been observed to fail without surfacing an + // error, leaving the app to boot the stale JavaScript baked into + // the installed .app payload with no indication anywhere that the + // sync was lost. After the transfer we therefore confirm the zip + // is actually present in the app sandbox; one retry covers + // transient AFC hiccups, and an unconfirmed delivery fails the + // sync loudly instead of printing "Successfully synced" over + // stale code (the run-controller surfaces the error; --clean + // reinstalls the full package). + // + // Presence alone is NOT sufficient evidence: a previous run that + // transferred the zip but aborted before the app restarted leaves + // a LEFTOVER sync.zip behind (the runtime only consumes it at + // boot), which would satisfy the check even when THIS upload + // failed. So any pre-existing zip is deleted up front — after + // that, post-transfer presence can only be produced by this run's + // upload. + // + // NOTE: deviceProjectRootPath is `.../LiveSync/app` (the extracted + // app folder); the zip is uploaded one level up, at the LiveSync + // root — the listing targets THAT directory. + // + // Escape hatch: NS_SKIP_IOS_SYNC_VERIFICATION=1 disables the + // whole verification for exotic setups where directory listing + // misbehaves but uploads are known-good. + const syncZipDevicePath = deviceAppData.deviceSyncZipPath; + const verificationSupported = + !!device.fileSystem.getDirectoryEntries && + process.env.NS_SKIP_IOS_SYNC_VERIFICATION !== "1"; + const listLiveSyncRoot = (): Promise => + device.fileSystem.getDirectoryEntries( + LiveSyncPaths.IOS_DEVICE_PROJECT_ROOT_PATH, + deviceAppData.appIdentifier, + ); + // Entry shape: ios-device-lib's native `read_dir` recursively joins + // `/` and returns FULL paths rooted at the + // requested directory (verified against IOSDeviceLib.cpp and live + // device output), so the canonical match is exact equality with + // `Library/Application Support/LiveSync/sync.zip`. The bare + // `"sync.zip"` equality is defensive cover for listing + // implementations that return root-relative names. Deliberately NO + // suffix matching — `endsWith("/sync.zip")` would false-positively + // accept a nested app asset like `.../LiveSync/app/sync.zip`. + const containsSyncZip = (entries: string[]): boolean => + entries.some( + (entry) => entry === syncZipDevicePath || entry === "sync.zip", + ); + // "delivered" / "missing" are definitive listings; "unknown" means + // the listing itself could not be read (after one retry). + const checkDelivery = async (): Promise< + "delivered" | "missing" | "unknown" + > => { + let entries = await listLiveSyncRoot(); + if (entries === null) { + entries = await listLiveSyncRoot(); + } + if (entries === null) { + return "unknown"; + } + return containsSyncZip(entries) ? "delivered" : "missing"; + }; + + let preListingAvailable = false; + let leftoverZipPresent = false; + if (verificationSupported) { + // Clear any leftover zip so the post-transfer check attributes + // presence to this run. Best-effort: AFC "file not found" is + // tolerated inside deleteFile. + await device.fileSystem.deleteFile( + syncZipDevicePath, + deviceAppData.appIdentifier, + ); + const preEntries = await listLiveSyncRoot(); + preListingAvailable = Array.isArray(preEntries); + leftoverZipPresent = preListingAvailable && containsSyncZip(preEntries); + } + + await transferSyncZip(); + + if (verificationSupported) { + if (leftoverZipPresent) { + // The pre-transfer delete did not take effect, so presence + // can no longer be attributed to this run. Most likely the + // upload succeeded too, but say so explicitly rather than + // claim verification. + this.$logger.warn( + "A leftover sync.zip from a previous run could not be removed — delivery verification for this sync is inconclusive. " + + "If the app runs stale code, re-run the command or use a clean rebuild (--clean).", + ); + } else { + let state = await checkDelivery(); + if (state === "missing") { + this.$logger.warn( + "sync.zip was not found on the device after transfer — retrying once...", + ); + await transferSyncZip(); + state = await checkDelivery(); + if (state === "delivered") { + this.$logger.info("sync.zip delivered on retry."); + } + } + if (state === "missing") { + throw new Error( + `Unable to deliver the application payload (sync.zip) to device ${device.deviceInfo.identifier}. ` + + `The app would run stale JavaScript without it. ` + + `Re-run the command, or use a clean rebuild (--clean) to reinstall the full application package.`, + ); + } + if (state === "unknown") { + if (preListingAvailable) { + // The listing worked moments before the upload and + // broke right after it — the AFC session is + // misbehaving at exactly the point where the upload + // itself is suspect. Fail closed. + throw new Error( + `Unable to confirm delivery of the application payload (sync.zip) to device ${device.deviceInfo.identifier}: ` + + `the device directory listing failed right after the transfer. ` + + `Re-run the command, or use a clean rebuild (--clean) to reinstall the full application package. ` + + `(Set NS_SKIP_IOS_SYNC_VERIFICATION=1 to bypass delivery verification.)`, + ); + } + // Listing was unavailable both before and after the + // transfer — verification is unsupported for this + // device/session. This is the single fail-open path, + // and it is loud rather than silent. + this.$logger.warn( + "Could not verify sync.zip delivery (device directory listing unavailable). " + + "If the transfer failed, the app will run stale JavaScript — re-run the command or use a clean rebuild (--clean).", + ); + } + } + } await deviceAppData.device.applicationManager.setTransferredAppFiles( - filesToTransfer + filesToTransfer, ); return { @@ -93,7 +232,7 @@ export class IOSLiveSyncService public async syncAfterInstall( device: Mobile.IDevice, - liveSyncInfo: ILiveSyncWatchInfo + liveSyncInfo: ILiveSyncWatchInfo, ): Promise { if (!device.isEmulator) { // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. @@ -109,11 +248,11 @@ export class IOSLiveSyncService protected _getDeviceLiveSyncService( device: Mobile.IDevice, - data: IProjectDir + data: IProjectDir, ): INativeScriptDeviceLiveSyncService { const service = this.$injector.resolve( IOSDeviceLiveSyncService, - { device, data } + { device, data }, ); return service; } diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 2fadc8fe16..43c843e59a 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -67,7 +67,7 @@ export class PluginsService implements IPluginsService { ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, }, - PluginsService.NPM_CONFIG + PluginsService.NPM_CONFIG, ); } @@ -80,7 +80,7 @@ export class PluginsService implements IPluginsService { private $filesHashService: IFilesHashService, private $injector: IInjector, private $mobileHelper: Mobile.IMobileHelper, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, ) {} public async add(plugin: string, projectData: IProjectData): Promise { @@ -97,26 +97,26 @@ export class PluginsService implements IPluginsService { await this.$packageManager.install( plugin, projectData.projectDir, - this.npmInstallOptions + this.npmInstallOptions, ) ).name; const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule( name, - projectData.projectDir + projectData.projectDir, ); const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson); if (realNpmPackageJson.nativescript) { const pluginData = this.convertToPluginData( realNpmPackageJson, - projectData.projectDir + projectData.projectDir, ); // Validate const action = async ( pluginDestinationPath: string, platform: constants.PlatformTypes, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { this.isPluginDataValidForPlatform(pluginData, platform, projectData); }; @@ -124,61 +124,61 @@ export class PluginsService implements IPluginsService { await this.executeForAllInstalledPlatforms(action, projectData); this.$logger.info( - `Successfully installed plugin ${realNpmPackageJson.name}.` + `Successfully installed plugin ${realNpmPackageJson.name}.`, ); } else { await this.$packageManager.uninstall( realNpmPackageJson.name, { save: true }, - projectData.projectDir + projectData.projectDir, ); this.$errors.fail( - `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.` + `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`, ); } } public async remove( pluginName: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { const removePluginNativeCodeAction = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { const pluginData = this.convertToPluginData( this.getNodeModuleData(pluginName, projectData.projectDir), - projectData.projectDir + projectData.projectDir, ); await platformData.platformProjectService.removePluginNativeCode( pluginData, - projectData + projectData, ); }; await this.executeForAllInstalledPlatforms( removePluginNativeCodeAction, - projectData + projectData, ); await this.executeNpmCommand( PluginsService.UNINSTALL_COMMAND_NAME, pluginName, - projectData + projectData, ); let showMessage = true; const action = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName)); this.$logger.info( - `Successfully removed plugin ${pluginName} for ${platform}.` + `Successfully removed plugin ${pluginName} for ${platform}.`, ); showMessage = false; }; @@ -194,7 +194,7 @@ export class PluginsService implements IPluginsService { plugin: string, version: string, isDev: boolean, - projectDir: string + projectDir: string, ) { const packageJsonPath = this.getPackageJsonFilePath(projectDir); let packageJsonContent = this.$fs.readJson(packageJsonPath); @@ -206,7 +206,7 @@ export class PluginsService implements IPluginsService { ) { const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent + packageJsonContent, ); packageJsonContent = result.packageJsonContent; } @@ -222,7 +222,7 @@ export class PluginsService implements IPluginsService { const packageJsonContent = this.$fs.readJson(packageJsonPath); const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent + packageJsonContent, ); if (result.hasModifiedPackageJson) { @@ -237,7 +237,7 @@ export class PluginsService implements IPluginsService { }: IPreparePluginNativeCodeData): Promise { const platformData = this.$platformsDataService.getPlatformData( platform, - projectData + projectData, ); const pluginPlatformsFolderPath = @@ -245,31 +245,37 @@ export class PluginsService implements IPluginsService { if (this.$fs.exists(pluginPlatformsFolderPath)) { const pathToPluginsBuildFile = path.join( platformData.projectRoot, - constants.PLUGINS_BUILD_DATA_FILENAME + constants.PLUGINS_BUILD_DATA_FILENAME, ); const allPluginsNativeHashes = this.getAllPluginsNativeHashes( - pathToPluginsBuildFile + pathToPluginsBuildFile, ); const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name]; const currentPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginPlatformsFolderPath, ); - if ( + const needsReprepare = !oldPluginNativeHashes || this.$filesHashService.hasChangesInShasums( oldPluginNativeHashes, - currentPluginNativeHashes - ) - ) { + currentPluginNativeHashes, + ) || + (platformData.platformProjectService.shouldRepreparePlugin?.( + pluginData, + projectData, + ) ?? + false); + + if (needsReprepare) { await platformData.platformProjectService.preparePluginNativeCode( pluginData, - projectData + projectData, ); const updatedPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginPlatformsFolderPath, ); this.setPluginNativeHashes({ @@ -283,13 +289,13 @@ export class PluginsService implements IPluginsService { } public async ensureAllDependenciesAreInstalled( - projectData: IProjectData + projectData: IProjectData, ): Promise { const packageJsonContent = this.$fs.readJson( - this.getPackageJsonFilePath(projectData.projectDir) + this.getPackageJsonFilePath(projectData.projectDir), ); const allDependencies = _.keys(packageJsonContent.dependencies).concat( - _.keys(packageJsonContent.devDependencies) + _.keys(packageJsonContent.devDependencies), ); const notInstalledDependencies = allDependencies @@ -316,7 +322,7 @@ export class PluginsService implements IPluginsService { "Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", - notInstalledDependencies + notInstalledDependencies, ); await this.$packageManager.install( projectData.projectDir, @@ -326,34 +332,34 @@ export class PluginsService implements IPluginsService { frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, - } + }, ); } } public async getAllInstalledPlugins( - projectData: IProjectData + projectData: IProjectData, ): Promise { const nodeModules = (await this.getAllInstalledModules(projectData)).map( (nodeModuleData) => - this.convertToPluginData(nodeModuleData, projectData.projectDir) + this.convertToPluginData(nodeModuleData, projectData.projectDir), ); return _.filter( nodeModules, - (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin + (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin, ); } public getAllProductionPlugins( projectData: IProjectData, platform: string, - dependencies?: IDependencyData[] + dependencies?: IDependencyData[], ): IPluginData[] { dependencies = dependencies || this.$nodeModulesDependenciesBuilder.getProductionDependencies( projectData.projectDir, - projectData.ignoredDependencies + projectData.ignoredDependencies, ); if (_.isEmpty(dependencies)) { @@ -361,12 +367,12 @@ export class PluginsService implements IPluginsService { } let productionPlugins: IDependencyData[] = dependencies.filter( - (d) => !!d.nativescript + (d) => !!d.nativescript, ); productionPlugins = this.ensureValidProductionPlugins( productionPlugins, projectData.projectDir, - platform + platform, ); return productionPlugins .map((plugin) => this.convertToPluginData(plugin, projectData.projectDir)) @@ -379,17 +385,17 @@ export class PluginsService implements IPluginsService { } public getDependenciesFromPackageJson( - projectDir: string + projectDir: string, ): IPackageJsonDepedenciesResult { const packageJson = this.$fs.readJson( - this.getPackageJsonFilePath(projectDir) + this.getPackageJsonFilePath(projectDir), ); const dependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.dependencies + packageJson.dependencies, ); const devDependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.devDependencies + packageJson.devDependencies, ); return { @@ -407,27 +413,27 @@ export class PluginsService implements IPluginsService { ( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ) => IDependencyData[] >( this._ensureValidProductionPlugins, ( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ) => { let key = _.sortBy(productionDependencies, (p) => p.directory) .map((d) => JSON.stringify(d, null, 2)) .join("\n"); key += projectDir + platform; return key; - } + }, ); private _ensureValidProductionPlugins( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ): IDependencyData[] { let clonedProductionDependencies = _.cloneDeep(productionDependencies); platform = platform.toLowerCase(); @@ -438,7 +444,7 @@ export class PluginsService implements IPluginsService { clonedProductionDependencies = this.ensureValidProductionPluginsForIOS( clonedProductionDependencies, projectDir, - platform + platform, ); } @@ -446,11 +452,11 @@ export class PluginsService implements IPluginsService { } private ensureValidProductionPluginsForAndroid( - productionDependencies: IDependencyData[] + productionDependencies: IDependencyData[], ): void { const dependenciesGroupedByName = _.groupBy( productionDependencies, - (p) => p.name + (p) => p.name, ); _.each( dependenciesGroupedByName, @@ -459,36 +465,36 @@ export class PluginsService implements IPluginsService { // the dependency exists multiple times in node_modules const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version + (g) => g.version, ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.trace( `Detected same versions (${_.first( - versions + versions, )}) of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}` + (d) => d.directory, + ).join(", ")}`, ); } else { this.$logger.trace( `Detected different versions of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}\nThis can cause build failures.` + (d) => d.directory, + ).join(", ")}\nThis can cause build failures.`, ); } } - } + }, ); } private ensureValidProductionPluginsForIOS( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ): IDependencyData[] { const dependenciesWithFrameworks: any[] = []; _.each(productionDependencies, (d) => { @@ -510,7 +516,7 @@ export class PluginsService implements IPluginsService { if (dependenciesWithFrameworks.length > 0) { const dependenciesGroupedByFrameworkName = _.groupBy( dependenciesWithFrameworks, - (d) => d.frameworkName + (d) => d.frameworkName, ); _.each( dependenciesGroupedByFrameworkName, @@ -519,16 +525,16 @@ export class PluginsService implements IPluginsService { // A framework exists multiple times in node_modules const groupedByName = _.groupBy( dependencyOccurrences, - (d) => d.name + (d) => d.name, ); const pluginsNames = _.keys(groupedByName); if (pluginsNames.length > 1) { // fail - the same framework is installed by different dependencies. const locations = dependencyOccurrences.map( - (d) => d.frameworkLocation + (d) => d.frameworkLocation, ); let msg = `Detected the framework ${frameworkName} is installed from multiple plugins at locations:\n${locations.join( - "\n" + "\n", )}\n`; msg += this.getHelpMessage(projectDir); this.$errors.fail(msg); @@ -537,33 +543,33 @@ export class PluginsService implements IPluginsService { const dependencyName = _.first(pluginsNames); const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version + (g) => g.version, ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.warn( `Detected the framework ${frameworkName} is installed multiple times from the same versions of plugin (${_.first( - versions + versions, )}) at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}` + (d) => d.directory, + ).join(", ")}`, ); const selectedPackage = _.minBy( dependencyOccurrences, - (d) => d.depth + (d) => d.depth, ); this.$logger.info( color.green( - `CLI will use only the native code from '${selectedPackage.directory}'.` - ) + `CLI will use only the native code from '${selectedPackage.directory}'.`, + ), ); _.each(dependencyOccurrences, (dependency) => { if (dependency !== selectedPackage) { productionDependencies.splice( productionDependencies.indexOf(dependency), - 1 + 1, ); } }); @@ -573,12 +579,12 @@ export class PluginsService implements IPluginsService { dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, - projectDir + projectDir, ); this.$errors.fail(message); } } - } + }, ); } @@ -589,13 +595,13 @@ export class PluginsService implements IPluginsService { dependencyName: string, frameworkName: string, dependencyOccurrencesGroupedByVersion: IDictionary, - projectDir: string + projectDir: string, ): string { let message = `Cannot use the same framework ${frameworkName} multiple times in your application. This framework comes from ${dependencyName} plugin, which is installed multiple times in node_modules:\n`; _.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => { message += dependencies.map( - (d) => `* Path: ${d.directory}, version: ${d.version}\n` + (d) => `* Path: ${d.directory}, version: ${d.version}\n`, ); }); @@ -621,7 +627,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private convertToPluginData( cacheData: IDependencyData | INodeModuleData, - projectDir: string + projectDir: string, ): IPluginData { try { const pluginData: IPluginData = {}; @@ -630,7 +636,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple pluginData.fullPath = (cacheData).directory || path.dirname( - this.getPackageJsonFilePathForModule(cacheData.name, projectDir) + this.getPackageJsonFilePathForModule(cacheData.name, projectDir), ); pluginData.isPlugin = !!cacheData.nativescript; pluginData.pluginPlatformsFolderPath = (platform: string) => { @@ -640,7 +646,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return path.join( pluginData.fullPath, "platforms", - platform.toLowerCase() + platform.toLowerCase(), ); }; const data = cacheData.nativescript; @@ -654,7 +660,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } catch (err) { this.$logger.trace( "NOTE: There appears to be a problem with this dependency:", - cacheData.name + cacheData.name, ); this.$logger.trace(err); return null; @@ -663,7 +669,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private removeDependencyFromPackageJsonContent( dependency: string, - packageJsonContent: any + packageJsonContent: any, ): { hasModifiedPackageJson: boolean; packageJsonContent: any } { let hasModifiedPackageJson = false; @@ -706,7 +712,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getPackageJsonFilePathForModule( moduleName: string, - projectDir: string + projectDir: string, ): string { const pathToJsonFile = resolvePackageJSONPath(moduleName, { paths: [projectDir], @@ -721,11 +727,21 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getNodeModuleData( module: string, - projectDir: string + projectDir: string, ): INodeModuleData { // module can be modulePath or moduleName if (!this.$fs.exists(module) || path.basename(module) !== "package.json") { - module = this.getPackageJsonFilePathForModule(module, projectDir); + const resolvedPath = this.getPackageJsonFilePathForModule( + module, + projectDir, + ); + if (!resolvedPath) { + this.$logger.warn( + `Could not find module ${color.yellow(module)}. It may have been removed or is not installed. Skipping.`, + ); + return null; + } + module = resolvedPath; } const data = this.$fs.readJson(module); @@ -741,37 +757,37 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private async ensure(projectData: IProjectData): Promise { await this.ensureAllDependenciesAreInstalled(projectData); this.$fs.ensureDirectoryExists( - this.getNodeModulesPath(projectData.projectDir) + this.getNodeModulesPath(projectData.projectDir), ); } private async getAllInstalledModules( - projectData: IProjectData + projectData: IProjectData, ): Promise { await this.ensure(projectData); const nodeModules = this.getDependencies(projectData.projectDir); return _.map(nodeModules, (nodeModuleName) => - this.getNodeModuleData(nodeModuleName, projectData.projectDir) - ); + this.getNodeModuleData(nodeModuleName, projectData.projectDir), + ).filter(Boolean); } private async executeNpmCommand( npmCommandName: string, npmCommandArguments: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) { await this.$packageManager.install( npmCommandArguments, projectData.projectDir, - this.npmInstallOptions + this.npmInstallOptions, ); } else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) { await this.$packageManager.uninstall( npmCommandArguments, PluginsService.NPM_CONFIG, - projectData.projectDir + projectData.projectDir, ); } @@ -786,31 +802,31 @@ This framework comes from ${dependencyName} plugin, which is installed multiple action: ( _pluginDestinationPath: string, pl: string, - _platformData: IPlatformData + _platformData: IPlatformData, ) => Promise, - projectData: IProjectData + projectData: IProjectData, ): Promise { const availablePlatforms = this.$mobileHelper.platformNames.map((p) => - p.toLowerCase() + p.toLowerCase(), ); for (const platform of availablePlatforms) { const isPlatformInstalled = this.$fs.exists( - path.join(projectData.platformsDir, platform.toLowerCase()) + path.join(projectData.platformsDir, platform.toLowerCase()), ); if (isPlatformInstalled) { const platformData = this.$platformsDataService.getPlatformData( platform.toLowerCase(), - projectData + projectData, ); const pluginDestinationPath = path.join( platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, - "tns_modules" + "tns_modules", ); await action( pluginDestinationPath, platform.toLowerCase(), - platformData + platformData, ); } } @@ -818,11 +834,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getInstalledFrameworkVersion( platform: constants.PlatformTypes, - projectData: IProjectData + projectData: IProjectData, ): string { const runtimePackage = this.$projectDataService.getRuntimePackage( projectData.projectDir, - platform + platform, ); // const platformData = this.$platformsDataService.getPlatformData(platform, projectData); // const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); @@ -832,27 +848,27 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private isPluginDataValidForPlatform( pluginData: IPluginData, platform: constants.PlatformTypes, - projectData: IProjectData + projectData: IProjectData, ): boolean { let isValid = true; const installedFrameworkVersion = this.getInstalledFrameworkVersion( platform, - projectData + projectData, ); const pluginPlatformsData = pluginData.platformsData; if (pluginPlatformsData) { const versionRequiredByPlugin = (pluginPlatformsData)[platform]; if (!versionRequiredByPlugin) { this.$logger.warn( - `${pluginData.name} is not supported for ${platform}.` + `${pluginData.name} is not supported for ${platform}.`, ); isValid = false; } else if ( semver.gt(versionRequiredByPlugin, installedFrameworkVersion) ) { this.$logger.warn( - `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.` + `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`, ); isValid = false; } @@ -862,7 +878,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private async getPluginNativeHashes( - pluginPlatformsDir: string + pluginPlatformsDir: string, ): Promise { let data: IStringDictionary = {}; if (this.$fs.exists(pluginPlatformsDir)) { @@ -875,7 +891,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private getAllPluginsNativeHashes( - pathToPluginsBuildFile: string + pathToPluginsBuildFile: string, ): IDictionary { if (this.$options.hostProjectPath) { // TODO: force rebuild plugins for now until we decide where to put .ns-plugins-build-data.json when embedding @@ -904,7 +920,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple opts.currentPluginNativeHashes; this.$fs.writeJson( opts.pathToPluginsBuildFile, - opts.allPluginsNativeHashes + opts.allPluginsNativeHashes, ); } } diff --git a/lib/services/project-backup-service.ts b/lib/services/project-backup-service.ts index b265d8dd00..0ea21be44c 100644 --- a/lib/services/project-backup-service.ts +++ b/lib/services/project-backup-service.ts @@ -8,7 +8,7 @@ export class ProjectBackupService implements IProjectBackupService { constructor( protected $fs: IFileSystem, protected $logger: ILogger, - protected $projectHelper: IProjectHelper + protected $projectHelper: IProjectHelper, ) {} getBackup(backupName: string): IBackup { @@ -25,12 +25,19 @@ export class ProjectBackupService implements IProjectBackupService { return backup.restore(); } - static Backup = class Backup implements IBackup { + // annotated so declaration emit has a nameable type: an anonymous class + // expression with private members cannot be described in a .d.ts + static Backup: new ( + $super: ProjectBackupService, + name: string, + pathsToBackup?: string[], + basePath?: string, + ) => IBackup = class Backup implements IBackup { constructor( private $super: ProjectBackupService, private name: string, private pathsToBackup: string[] = [], - private basePath: string = $super.$projectHelper.projectDir + private basePath: string = $super.$projectHelper.projectDir, ) {} get backupDir() { @@ -49,7 +56,7 @@ export class ProjectBackupService implements IProjectBackupService { const targetPath = path.resolve(this.backupDir, pathToBackup); if (this.$super.$fs.exists(sourcePath)) { this.$super.$logger.trace( - `BACKING UP ${color.cyan(sourcePath)} -> ${color.green(targetPath)}` + `BACKING UP ${color.cyan(sourcePath)} -> ${color.green(targetPath)}`, ); this.$super.$fs.copyFile(sourcePath, targetPath); backedUpPaths.push(pathToBackup); @@ -76,7 +83,7 @@ export class ProjectBackupService implements IProjectBackupService { const sourcePath = path.resolve(this.backupDir, pathToBackup); const targetPath = path.resolve(this.basePath, pathToBackup); this.$super.$logger.trace( - `RESTORING ${color.green(sourcePath)} -> ${color.cyan(targetPath)}` + `RESTORING ${color.green(sourcePath)} -> ${color.cyan(targetPath)}`, ); if (this.$super.$fs.exists(sourcePath)) { this.$super.$fs.copyFile(sourcePath, targetPath); @@ -110,7 +117,7 @@ export class ProjectBackupService implements IProjectBackupService { remove() { if (!this.$super.$fs.exists(this.backupDir)) { this.$super.$logger.trace( - `No backup named ${this.name} could be found.` + `No backup named ${this.name} could be found.`, ); return; } @@ -135,7 +142,7 @@ export class ProjectBackupService implements IProjectBackupService { private getBackupData(): { name: string; paths: string[] } { if (!this.$super.$fs.exists(this.backupDir)) { this.$super.$logger.trace( - `No backup named ${this.name} could be found.` + `No backup named ${this.name} could be found.`, ); return; } @@ -143,7 +150,7 @@ export class ProjectBackupService implements IProjectBackupService { if (!this.$super.$fs.exists(backupJSONPath)) { this.$super.$logger.trace( - `The backup ${this.name} does not contain a _backup.json.` + `The backup ${this.name} does not contain a _backup.json.`, ); return; } diff --git a/lib/services/project-cleanup-service.ts b/lib/services/project-cleanup-service.ts index 25d7b01edf..85898c6492 100644 --- a/lib/services/project-cleanup-service.ts +++ b/lib/services/project-cleanup-service.ts @@ -20,12 +20,12 @@ export class ProjectCleanupService implements IProjectCleanupService { private $fs: IFileSystem, private $logger: ILogger, private $projectHelper: IProjectHelper, - private $terminalSpinnerService: ITerminalSpinnerService + private $terminalSpinnerService: ITerminalSpinnerService, ) {} public async clean( pathsToClean: string[], - options?: IProjectCleanupOptions + options?: IProjectCleanupOptions, ): Promise { this.spinner = this.$terminalSpinnerService.createSpinner({ isSilent: options?.silent, @@ -39,10 +39,10 @@ export class ProjectCleanupService implements IProjectCleanupService { (error) => { this.$logger.trace( `Encountered error while cleaning. Error is: ${error.message}.`, - error + error, ); return { ok: false }; - } + }, ); if (stats && "size" in cleanRes) { stats.set(pathToClean, cleanRes.size); @@ -63,7 +63,7 @@ export class ProjectCleanupService implements IProjectCleanupService { public async cleanPath( pathToClean: string, - options?: IProjectCleanupOptions + options?: IProjectCleanupOptions, ): Promise { const dryRun = options?.dryRun ?? false; const logPrefix = dryRun ? color.grey("(dry run) ") : ""; @@ -77,9 +77,21 @@ export class ProjectCleanupService implements IProjectCleanupService { } const filePath = path.resolve(this.$projectHelper.projectDir, pathToClean); - const displayPath = color.yellow( - `${path.relative(this.$projectHelper.projectDir, filePath)}` + const relativePath = path.relative( + this.$projectHelper.projectDir, + filePath, ); + const displayPath = color.yellow(`${relativePath}`); + + // Paths reach here from the project config - buildPath and + // cli.pathsToClean among them - where a leading `..` resolves onto + // directories the project does not own. + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + this.$logger.warn( + `Skipping '${filePath}' because it is outside the project directory.`, + ); + return { ok: false }; + } this.$logger.trace(`${logPrefix}Trying to clean '${filePath}'`); @@ -93,13 +105,13 @@ export class ProjectCleanupService implements IProjectCleanupService { if (stat.isDirectory()) { this.$logger.trace( - `${logPrefix}Path '${filePath}' is a directory, deleting.` + `${logPrefix}Path '${filePath}' is a directory, deleting.`, ); !dryRun && this.$fs.deleteDirectorySafe(filePath); fileType = "directory"; } else { this.$logger.trace( - `${logPrefix}Path '${filePath}' is a file, deleting.` + `${logPrefix}Path '${filePath}' is a file, deleting.`, ); !dryRun && this.$fs.deleteFile(filePath); fileType = "file"; @@ -122,7 +134,7 @@ export class ProjectCleanupService implements IProjectCleanupService { this.$logger.trace(`${logPrefix}Path '${filePath}' not found, skipping.`); this.spinner.info( - `${logPrefix}Skipping ${displayPath} because it doesn't exist.` + `${logPrefix}Skipping ${displayPath} because it doesn't exist.`, ); if (options?.stats) { diff --git a/lib/services/project-config-service.ts b/lib/services/project-config-service.ts index ac3f838ac4..2b752dd3ec 100644 --- a/lib/services/project-config-service.ts +++ b/lib/services/project-config-service.ts @@ -23,15 +23,13 @@ import { import { IBasePluginData } from "../definitions/plugins"; import { injector } from "../common/yok"; import { EOL } from "os"; -import { - format as prettierFormat, - resolveConfig as resolvePrettierConfig, -} from "prettier"; import { cache, exported } from "../common/decorators"; import { IOptions } from "../declarations"; import * as semver from "semver/preload"; import { ICleanupService } from "../definitions/cleanup-service"; +type PrettierModule = typeof import("prettier"); + export class ProjectConfigService implements IProjectConfigService { private forceUsingNewConfig: boolean = false; private forceUsingLegacyConfig: boolean = false; @@ -93,7 +91,10 @@ export default { ); } - public detectProjectConfigs(projectDir?: string): IProjectConfigInformation { + public detectProjectConfigs( + projectDir?: string, + options?: { suppressWarnings?: boolean }, + ): IProjectConfigInformation { // allow overriding config name with env variable or --config (or -c) let configName: string | boolean = process.env.NATIVESCRIPT_CONFIG_NAME ?? this.$options.config; @@ -156,9 +157,11 @@ export default { const hasNSConfig = !!NSConfigPath && hasExistingConfig; const usingNSConfig = !(hasTSConfig || hasJSConfig); - if (hasTSConfig && hasJSConfig) { + if (hasTSConfig && hasJSConfig && !options?.suppressWarnings) { this.$logger.warn( - `You have both a ${CONFIG_FILE_NAME_JS} and ${CONFIG_FILE_NAME_TS} file. Defaulting to ${CONFIG_FILE_NAME_TS}.`, + `You have both a ${CONFIG_FILE_NAME_JS} and ${CONFIG_FILE_NAME_TS} file in ${path.dirname( + TSConfigPath, + )}. Defaulting to ${CONFIG_FILE_NAME_TS}.`, ); } @@ -174,8 +177,11 @@ export default { } @exported("projectConfigService") - public readConfig(projectDir?: string): INsConfig { - const info = this.detectProjectConfigs(projectDir); + public readConfig( + projectDir?: string, + options?: { suppressWarnings?: boolean }, + ): INsConfig { + const info = this.detectProjectConfigs(projectDir, options); if ( this.forceUsingLegacyConfig || @@ -273,27 +279,7 @@ export default { configContent, ); const newContent = transformer.setValue(key, value); - const prettierOptions = (await resolvePrettierConfig( - this.projectHelper.projectDir, - { editorconfig: true }, - )) || { - semi: false, - singleQuote: true, - }; - this.$logger.trace( - "updating config, prettier options: ", - prettierOptions, - ); - this.$fs.writeFile( - configFilePath, - await prettierFormat(newContent, { - ...prettierOptions, - parser: "typescript", - // note: we don't use plugins here, since we are only formatting ts files, and they are supported by default - // and this also causes issues with certain plugins, like prettier-plugin-tailwindcss. - plugins: [], - }), - ); + this.$fs.writeFile(configFilePath, await this.formatConfig(newContent)); } catch (error) { this.$logger.error(`Failed to update config.` + error); } finally { @@ -318,6 +304,61 @@ export default { } } + /** + * Resolved from the project first: the formatting options come from the + * project's own prettier/editorconfig setup, so the project's prettier version + * is the one that understands them. Required lazily - a broken or missing + * prettier must not take down the CLI, since this is a core service. + */ + private loadPrettier(): PrettierModule { + const projectDir = this.projectHelper.projectDir; + + if (projectDir) { + try { + return require(require.resolve("prettier", { paths: [projectDir] })); + } catch (error) { + this.$logger.trace( + "Could not load prettier from the project, using the bundled one.", + error, + ); + } + } + + return require("prettier"); + } + + private async formatConfig(content: string): Promise { + try { + const prettier = this.loadPrettier(); + const prettierOptions = (await prettier.resolveConfig( + this.projectHelper.projectDir, + { editorconfig: true }, + )) || { + semi: false, + singleQuote: true, + }; + this.$logger.trace( + "updating config, prettier options: ", + prettierOptions, + ); + + // awaiting covers both prettier 2 (sync) and prettier 3 (async) format + return await prettier.format(content, { + ...prettierOptions, + parser: "typescript", + // note: we don't use plugins here, since we are only formatting ts files, and they are supported by default + // and this also causes issues with certain plugins, like prettier-plugin-tailwindcss. + plugins: [], + }); + } catch (error) { + this.$logger.warn( + "Could not format the config with prettier - it will be written unformatted.", + ); + this.$logger.trace("prettier failed with: ", error); + return content; + } + } + public writeDefaultConfig(projectDir: string, appId?: string) { const TSConfigPath = path.resolve(projectDir, CONFIG_FILE_NAME_TS); diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 52f1ff49a5..31e5a2a342 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -50,9 +50,10 @@ export class ProjectDataService implements IProjectDataService { private $fs: IFileSystem, private $staticConfig: IStaticConfig, private $logger: ILogger, + private $projectData: IProjectData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $androidResourcesMigrationService: IAndroidResourcesMigrationService, - private $injector: IInjector + private $injector: IInjector, ) { try { // add the ProjectData of the default projectDir to the projectData cache @@ -72,7 +73,7 @@ export class ProjectDataService implements IProjectDataService { public getNSValue(projectDir: string, propertyName: string): any { return this.getValue( projectDir, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } @@ -80,11 +81,11 @@ export class ProjectDataService implements IProjectDataService { try { return this.getPropertyValueFromJson( jsonData, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } catch (e) { this.$logger.trace( - "Failed to get NS property value from JSON project data." + "Failed to get NS property value from JSON project data.", ); } @@ -98,7 +99,7 @@ export class ProjectDataService implements IProjectDataService { public removeNSProperty(projectDir: string, propertyName: string): void { this.removeProperty( projectDir, - this.getNativeScriptPropertyName(propertyName) + this.getNativeScriptPropertyName(propertyName), ); } @@ -109,7 +110,7 @@ export class ProjectDataService implements IProjectDataService { ][dependencyName]; this.$fs.writeJson( projectFileInfo.projectFilePath, - projectFileInfo.projectData + projectFileInfo.projectData, ); } @@ -128,7 +129,7 @@ export class ProjectDataService implements IProjectDataService { @exported("projectDataService") public getProjectDataFromContent( packageJsonContent: string, - projectDir?: string + projectDir?: string, ): IProjectData { projectDir = projectDir || this.defaultProjectDir; this.projectDataCache[projectDir] = @@ -136,25 +137,25 @@ export class ProjectDataService implements IProjectDataService { this.$injector.resolve(ProjectData); this.projectDataCache[projectDir].initializeProjectDataFromContent( packageJsonContent, - projectDir + projectDir, ); return this.projectDataCache[projectDir]; } @exported("projectDataService") public async getAssetsStructure( - opts: IProjectDir + opts: IProjectDir, ): Promise { const iOSAssetStructure = await this.getIOSAssetsStructure(opts); const androidAssetStructure = await this.getAndroidAssetsStructure(opts); this.$logger.trace( "iOS Assets structure:", - JSON.stringify(iOSAssetStructure, null, 2) + JSON.stringify(iOSAssetStructure, null, 2), ); this.$logger.trace( "Android Assets structure:", - JSON.stringify(androidAssetStructure, null, 2) + JSON.stringify(androidAssetStructure, null, 2), ); return { @@ -171,30 +172,30 @@ export class ProjectDataService implements IProjectDataService { const basePath = path.join( projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.iOS, - AssetConstants.iOSAssetsDirName + AssetConstants.iOSAssetsDirName, ); const pathToIcons = path.join(basePath, AssetConstants.iOSIconsDirName); const icons = await this.getIOSAssetSubGroup(pathToIcons); const pathToSplashBackgrounds = path.join( basePath, - AssetConstants.iOSSplashBackgroundsDirName + AssetConstants.iOSSplashBackgroundsDirName, ); const splashBackgrounds = await this.getIOSAssetSubGroup( - pathToSplashBackgrounds + pathToSplashBackgrounds, ); const pathToSplashCenterImages = path.join( basePath, - AssetConstants.iOSSplashCenterImagesDirName + AssetConstants.iOSSplashCenterImagesDirName, ); const splashCenterImages = await this.getIOSAssetSubGroup( - pathToSplashCenterImages + pathToSplashCenterImages, ); const pathToSplashImages = path.join( basePath, - AssetConstants.iOSSplashImagesDirName + AssetConstants.iOSSplashImagesDirName, ); const splashImages = await this.getIOSAssetSubGroup(pathToSplashImages); @@ -208,7 +209,7 @@ export class ProjectDataService implements IProjectDataService { public removeNSConfigProperty( projectDir: string, - propertyName: string + propertyName: string, ): void { this.$logger.trace(`Removing "${propertyName}" property from nsconfig.`); this.updateNsConfigValue(projectDir, null, [propertyName]); @@ -217,7 +218,7 @@ export class ProjectDataService implements IProjectDataService { @exported("projectDataService") public async getAndroidAssetsStructure( - opts: IProjectDir + opts: IProjectDir, ): Promise { // TODO: Use image-size package to get the width and height of an image. // TODO: Parse the splash_screen.xml in nodpi directory and get from it the names of the background and center image. @@ -227,10 +228,10 @@ export class ProjectDataService implements IProjectDataService { const projectData = this.getProjectData(projectDir); const pathToAndroidDir = path.join( projectData.appResourcesDirectoryPath, - this.$devicePlatformsConstants.Android + this.$devicePlatformsConstants.Android, ); const hasMigrated = this.$androidResourcesMigrationService.hasMigrated( - projectData.appResourcesDirectoryPath + projectData.appResourcesDirectoryPath, ); const basePath = hasMigrated ? path.join(pathToAndroidDir, SRC_DIR, MAIN_DIR, RESOURCES_DIR) @@ -239,7 +240,7 @@ export class ProjectDataService implements IProjectDataService { let useLegacy = false; try { const manifest = this.$fs.readText( - path.resolve(basePath, "../AndroidManifest.xml") + path.resolve(basePath, "../AndroidManifest.xml"), ); useLegacy = !manifest.includes(`android:icon="@mipmap/ic_launcher"`); } catch (err) { @@ -253,11 +254,11 @@ export class ProjectDataService implements IProjectDataService { icons: this.getAndroidAssetSubGroup(content.icons, basePath), splashBackgrounds: this.getAndroidAssetSubGroup( content.splashBackgrounds, - basePath + basePath, ), splashCenterImages: this.getAndroidAssetSubGroup( content.splashCenterImages, - basePath + basePath, ), splashImages: null, }; @@ -276,7 +277,7 @@ export class ProjectDataService implements IProjectDataService { const pathToProjectNodeModules = path.join( projectDir, - NODE_MODULES_FOLDER_NAME + NODE_MODULES_FOLDER_NAME, ); const files = this.$fs.enumerateFilesInDirectorySync( projectData.appDirectoryPath, @@ -296,7 +297,7 @@ export class ProjectDataService implements IProjectDataService { } return path.extname(filePath) === supportedFileExtension; - } + }, ); return files; @@ -311,7 +312,7 @@ export class ProjectDataService implements IProjectDataService { private updateNsConfigValue( projectDir: string, updateObject?: INsConfig, - propertiesToRemove?: string[] + propertiesToRemove?: string[], ): void { // todo: figure out a way to update js/ts configs // most likely needs an ast parser/writer @@ -322,7 +323,7 @@ export class ProjectDataService implements IProjectDataService { if (updateObject) { newNsConfig = _.assign( newNsConfig || this.getNsConfigDefaultObject(), - updateObject + updateObject, ); } @@ -345,7 +346,7 @@ export class ProjectDataService implements IProjectDataService { } catch (e) { this.$logger.trace( "The `nsconfig` content is not a valid JSON. Parse error: ", - e + e, ); } } @@ -360,7 +361,7 @@ export class ProjectDataService implements IProjectDataService { "..", CLI_RESOURCES_DIR_NAME, AssetConstants.assets, - AssetConstants.imageDefinitionsFileName + AssetConstants.imageDefinitionsFileName, ); const imageDefinitions = this.$fs.readJson(pathToImageDefinitions); @@ -370,7 +371,7 @@ export class ProjectDataService implements IProjectDataService { private async getIOSAssetSubGroup(dirPath: string): Promise { const pathToContentJson = path.join( dirPath, - AssetConstants.iOSResourcesFileName + AssetConstants.iOSResourcesFileName, ); const content = (this.$fs.exists(pathToContentJson) && this.$fs.readJson(pathToContentJson)) || { images: [] }; @@ -404,7 +405,7 @@ export class ProjectDataService implements IProjectDataService { assetSubGroup, (assetElement) => assetElement.filename === image.filename && - path.basename(assetElement.directory) === path.basename(dirPath) + path.basename(assetElement.directory) === path.basename(dirPath), ); if (assetItem) { @@ -434,20 +435,20 @@ export class ProjectDataService implements IProjectDataService { this.$logger.trace( "Missing data for image", image, - " in CLI's resource file, but we will try to generate images based on the size from Contents.json" + " in CLI's resource file, but we will try to generate images based on the size from Contents.json", ); finalContent.images.push(image); } else if (image.filename) { this.$logger.warn( `Didn't find a matching image definition for file ${path.join( path.basename(dirPath), - image.filename - )}. This file will be skipped from resources generation.` + image.filename, + )}. This file will be skipped from resources generation.`, ); } else { this.$logger.trace( `Unable to detect data for image generation of image`, - image + image, ); } } @@ -458,7 +459,7 @@ export class ProjectDataService implements IProjectDataService { private getAndroidAssetSubGroup( assetItems: IAssetItem[], - basePath: string + basePath: string, ): IAssetSubGroup { const assetSubGroup: IAssetSubGroup = { images: [], @@ -468,7 +469,7 @@ export class ProjectDataService implements IProjectDataService { const imagePath = path.join( basePath, assetItem.directory, - assetItem.filename + assetItem.filename, ); assetItem.path = imagePath; if (assetItem.width && assetItem.height) { @@ -489,7 +490,7 @@ export class ProjectDataService implements IProjectDataService { } catch (err) { this.$logger.trace( `Error while trying to get property ${propertyName} from ${projectDir}. Error is:`, - err + err, ); } } @@ -503,10 +504,10 @@ export class ProjectDataService implements IProjectDataService { private getPropertyValueFromJson( jsonData: any, - dottedPropertyName: string + dottedPropertyName: string, ): any { const props = dottedPropertyName.split( - NATIVESCRIPT_PROPS_INTERNAL_DELIMITER + NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, ); let result = jsonData[props.shift()]; @@ -555,7 +556,7 @@ export class ProjectDataService implements IProjectDataService { private getProjectFileData(projectDir: string): IProjectFileData { const projectFilePath = path.join( projectDir, - this.$staticConfig.PROJECT_FILE_NAME + this.$staticConfig.PROJECT_FILE_NAME, ); const projectFileContent = this.$fs.readText(projectFilePath); const projectData = projectFileContent @@ -577,11 +578,11 @@ export class ProjectDataService implements IProjectDataService { public getRuntimePackage( projectDir: string, - platform: constants.SupportedPlatform + platform: constants.SupportedPlatform, ): IBasePluginData { platform = platform.toLowerCase() as constants.SupportedPlatform; const packageJson = this.$fs.readJson( - path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME) + path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME), ); const runtimeName = platform === PlatformTypes.android @@ -622,21 +623,23 @@ export class ProjectDataService implements IProjectDataService { }) private getInstalledRuntimePackage( projectDir: string, - platform: constants.SupportedPlatform + platform: constants.SupportedPlatform, ): IBasePluginData { const runtimePackage = this.$pluginsService .getDependenciesFromPackageJson(projectDir) .devDependencies.find((d) => { if (platform === constants.PlatformTypes.ios) { - return [ - constants.SCOPED_IOS_RUNTIME_NAME, - constants.TNS_IOS_RUNTIME_NAME, - ].includes(d.name); + const packageName = + this.$projectData.nsConfig?.ios?.runtimePackageName || + constants.SCOPED_IOS_RUNTIME_NAME; + return [packageName, constants.TNS_IOS_RUNTIME_NAME].includes(d.name); } else if (platform === constants.PlatformTypes.android) { - return [ - constants.SCOPED_ANDROID_RUNTIME_NAME, - constants.TNS_ANDROID_RUNTIME_NAME, - ].includes(d.name); + const packageName = + this.$projectData.nsConfig?.android?.runtimePackageName || + constants.SCOPED_ANDROID_RUNTIME_NAME; + return [packageName, constants.TNS_ANDROID_RUNTIME_NAME].includes( + d.name, + ); } else if (platform === constants.PlatformTypes.visionos) { return d.name === constants.SCOPED_VISIONOS_RUNTIME_NAME; } @@ -654,7 +657,7 @@ export class ProjectDataService implements IProjectDataService { runtimePackage.name, { paths: [projectDir], - } + }, ); if (!runtimePackageJsonPath) { @@ -663,12 +666,12 @@ export class ProjectDataService implements IProjectDataService { } runtimePackage.version = this.$fs.readJson( - runtimePackageJsonPath + runtimePackageJsonPath, ).version; } catch (err) { if (isRange) { runtimePackage.version = semver.coerce( - runtimePackage.version + runtimePackage.version, ).version; (runtimePackage as any)._coerced = true; @@ -683,16 +686,20 @@ export class ProjectDataService implements IProjectDataService { // default to the scoped runtimes this.$logger.trace( - "Could not find an installed runtime, falling back to default runtimes" + "Could not find an installed runtime, falling back to default runtimes", ); if (platform === constants.PlatformTypes.ios) { return { - name: constants.SCOPED_IOS_RUNTIME_NAME, + name: + this.$projectData.nsConfig?.ios?.runtimePackageName || + constants.SCOPED_IOS_RUNTIME_NAME, version: null, }; } else if (platform === constants.PlatformTypes.android) { return { - name: constants.SCOPED_ANDROID_RUNTIME_NAME, + name: + this.$projectData.nsConfig?.android?.runtimePackageName || + constants.SCOPED_ANDROID_RUNTIME_NAME, version: null, }; } else if (platform === constants.PlatformTypes.visionos) { diff --git a/lib/services/project-name-service.ts b/lib/services/project-name-service.ts index 5b595d207e..d4d9b17ffe 100644 --- a/lib/services/project-name-service.ts +++ b/lib/services/project-name-service.ts @@ -1,20 +1,20 @@ import { isInteractive } from "../common/helpers"; -import { IProjectNameService } from "../declarations"; import { IErrors } from "../common/declarations"; import * as _ from "lodash"; +import { ProjectNameService } from "../contracts/project-name-service"; import { injector } from "../common/yok"; -export class ProjectNameService implements IProjectNameService { +export class ProjectNameServiceImpl implements ProjectNameService { constructor( private $projectNameValidator: IProjectNameValidator, private $errors: IErrors, private $logger: ILogger, - private $prompter: IPrompter + private $prompter: IPrompter, ) {} public async ensureValidName( projectName: string, - validateOptions?: { force: boolean } + validateOptions?: { force: boolean }, ): Promise { if (validateOptions && validateOptions.force) { return projectName; @@ -24,7 +24,7 @@ export class ProjectNameService implements IProjectNameService { return await this.promptForNewName( "The project name is invalid.", projectName, - validateOptions + validateOptions, ); } @@ -33,28 +33,28 @@ export class ProjectNameService implements IProjectNameService { if (!this.checkIfNameStartsWithLetter(projectName)) { if (!userCanInteract) { this.$errors.fail( - "The project name does not start with letter and will fail to build for Android. If You want to create project with this name add --force to the create command." + "The project name does not start with letter and will fail to build for Android. If You want to create project with this name add --force to the create command.", ); } return await this.promptForNewName( "The project name does not start with letter and will fail to build for Android.", projectName, - validateOptions + validateOptions, ); } if (projectName.toUpperCase() === "APP") { if (!userCanInteract) { this.$errors.fail( - "You cannot build applications named 'app' in Xcode. Consider creating a project with different name. If You want to create project with this name add --force to the create command." + "You cannot build applications named 'app' in Xcode. Consider creating a project with different name. If You want to create project with this name add --force to the create command.", ); } return await this.promptForNewName( "You cannot build applications named 'app' in Xcode. Consider creating a project with different name.", projectName, - validateOptions + validateOptions, ); } @@ -69,27 +69,27 @@ export class ProjectNameService implements IProjectNameService { private async promptForNewName( warningMessage: string, projectName: string, - validateOptions?: { force: boolean } + validateOptions?: { force: boolean }, ): Promise { if (await this.promptForForceNameConfirm(warningMessage)) { return projectName; } const newProjectName = await this.$prompter.getString( - "Enter the new project name:" + "Enter the new project name:", ); return await this.ensureValidName(newProjectName, validateOptions); } private async promptForForceNameConfirm( - warningMessage: string + warningMessage: string, ): Promise { this.$logger.warn(warningMessage); return await this.$prompter.confirm( - "Do you want to create the project with this name?" + "Do you want to create the project with this name?", ); } } -injector.register("projectNameService", ProjectNameService); +injector.register("projectNameService", ProjectNameServiceImpl); diff --git a/lib/services/qr-code-terminal-service.ts b/lib/services/qr-code-terminal-service.ts deleted file mode 100644 index 1933beac30..0000000000 --- a/lib/services/qr-code-terminal-service.ts +++ /dev/null @@ -1,15 +0,0 @@ -const qrcode = require("qrcode-terminal"); -import { injector } from "../common/yok"; - -export class QrCodeTerminalService implements IQrCodeTerminalService { - constructor(private $logger: ILogger) {} - - public generate(url: string): void { - try { - qrcode.generate(url); - } catch (err) { - this.$logger.info(`Failed to generate QR code for ${url}`, err); - } - } -} -injector.register("qrCodeTerminalService", QrCodeTerminalService); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 7cd37fe2c1..8ba4eda276 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -9,12 +9,8 @@ import { } from "../definitions/project"; import { IConfiguration, IOptions } from "../declarations"; import { IPluginsService } from "../definitions/plugins"; -import { - Server, - IFileSystem, - IChildProcess, - ErrorCodes, -} from "../common/declarations"; +import { Server, IFileSystem, IChildProcess } from "../common/declarations"; +import { ErrorCodes } from "../common/enums"; import * as _ from "lodash"; import { injector } from "../common/yok"; import { ICommandParameter } from "../common/definitions/commands"; @@ -39,7 +35,7 @@ export class TestExecutionService implements ITestExecutionService { private $options: IOptions, private $pluginsService: IPluginsService, private $projectDataService: IProjectDataService, - private $childProcess: IChildProcess + private $childProcess: IChildProcess, ) {} public platform: string; @@ -47,13 +43,13 @@ export class TestExecutionService implements ITestExecutionService { public async startKarmaServer( platform: string, liveSyncInfo: ILiveSyncInfo, - deviceDescriptors: ILiveSyncDeviceDescriptor[] + deviceDescriptors: ILiveSyncDeviceDescriptor[], ): Promise { platform = platform.toLowerCase(); this.platform = platform; const projectData = this.$projectDataService.getProjectData( - liveSyncInfo.projectDir + liveSyncInfo.projectDir, ); // We need the dependencies installed here, so we can start the Karma server. @@ -64,12 +60,12 @@ export class TestExecutionService implements ITestExecutionService { const karmaRunner = this.$childProcess.spawn( process.execPath, [path.join(__dirname, "karma-execution.js")], - { stdio: ["inherit", "inherit", "inherit", "ipc"] } + { stdio: ["inherit", "inherit", "inherit", "ipc"] }, ); const launchKarmaTests = async (karmaData: any) => { this.$logger.trace( "## Unit-testing: Parent process received message", - karmaData + karmaData, ); let port: string; if (karmaData.url) { @@ -80,23 +76,23 @@ export class TestExecutionService implements ITestExecutionService { this.$fs.writeFile( path.join( liveSyncInfo.projectDir, - TestExecutionService.SOCKETIO_JS_FILE_NAME + TestExecutionService.SOCKETIO_JS_FILE_NAME, ), - JSON.parse(socketIoJs) + JSON.parse(socketIoJs), ); } if (karmaData.launcherConfig) { const configOptions: IKarmaConfigOptions = JSON.parse( - karmaData.launcherConfig + karmaData.launcherConfig, ); const configJs = this.generateConfig(port, configOptions); this.$fs.writeFile( path.join( liveSyncInfo.projectDir, - TestExecutionService.CONFIG_FILE_NAME + TestExecutionService.CONFIG_FILE_NAME, ), - configJs + configJs, ); } @@ -137,7 +133,7 @@ export class TestExecutionService implements ITestExecutionService { } public async canStartKarmaServer( - projectData: IProjectData + projectData: IProjectData, ): Promise { let canStartKarmaServer = true; const requiredDependencies = ["@nativescript/unit-test-runner"]; // we need @nativescript/unit-test-runner at the local level because of hooks! @@ -165,8 +161,8 @@ export class TestExecutionService implements ITestExecutionService { .map( (nicName) => nics[nicName].filter( - (binding: any) => binding.family === "IPv4" || binding.family === 4 - )[0] + (binding: any) => binding.family === "IPv4" || binding.family === 4, + )[0], ) .filter((binding) => !!binding) .map((binding) => binding.address); @@ -182,7 +178,7 @@ export class TestExecutionService implements ITestExecutionService { private getKarmaConfiguration( platform: string, - projectData: IProjectData + projectData: IProjectData, ): any { const karmaConfig: any = { browsers: [platform], diff --git a/lib/services/test-initialization-service.ts b/lib/services/test-initialization-service.ts index 166ca77da5..ba70e1c051 100644 --- a/lib/services/test-initialization-service.ts +++ b/lib/services/test-initialization-service.ts @@ -13,38 +13,48 @@ import { injector } from "../common/yok"; export class TestInitializationService implements ITestInitializationService { private configsPath = path.join(__dirname, "..", "..", "config"); - constructor(private $errors: IErrors, private $fs: IFileSystem) {} + constructor( + private $errors: IErrors, + private $fs: IFileSystem, + ) {} @cache() public getDependencies(selectedFramework: string): IDependencyInformation[] { const dependenciesPath = path.join( this.configsPath, - "test-dependencies.json" + "test-dependencies.json", ); const allDependencies: { name: string; framework?: string; + frameworks?: string[]; excludedPeerDependencies?: string[]; }[] = this.$fs.readJson(dependenciesPath); const dependenciesVersionsPath = path.join( this.configsPath, - "test-deps-versions-generated.json" + "test-deps-versions-generated.json", ); const dependenciesVersions = this.$fs.readJson(dependenciesVersionsPath); - const targetFrameworkDependencies: IDependencyInformation[] = allDependencies - .filter( - (dependency) => - !dependency.framework || dependency.framework === selectedFramework - ) - .map((dependency) => { - const dependencyVersion = dependenciesVersions[dependency.name]; - if (!dependencyVersion) { - this.$errors.fail(`'${dependency}' is not a registered dependency.`); - } - return { ...dependency, version: dependencyVersion }; - }); + const targetFrameworkDependencies: IDependencyInformation[] = + allDependencies + .filter( + (dependency) => + dependency.framework === selectedFramework || + (dependency.frameworks && + dependency.frameworks.indexOf(selectedFramework) !== -1) || + (!dependency.framework && !dependency.frameworks), + ) + .map((dependency) => { + const dependencyVersion = dependenciesVersions[dependency.name]; + if (!dependencyVersion) { + this.$errors.fail( + `'${dependency}' is not a registered dependency.`, + ); + } + return { ...dependency, version: dependencyVersion }; + }); return targetFrameworkDependencies; } @@ -56,12 +66,18 @@ export class TestInitializationService implements ITestInitializationService { public getFrameworkNames(): string[] { const configsPath = path.join(__dirname, "..", "..", "config"); const dependenciesPath = path.join(configsPath, "test-dependencies.json"); - const allDependencies: { name: string; framework?: string }[] = JSON.parse( - fs.readFileSync(dependenciesPath, { encoding: "utf-8" }) + const allDependencies: { + name: string; + framework?: string; + frameworks?: string[]; + }[] = JSON.parse(fs.readFileSync(dependenciesPath, { encoding: "utf-8" })); + const frameworks = _.uniq( + _.flatten( + allDependencies.map( + (item) => item.frameworks || (item.framework ? [item.framework] : []), + ), + ), ); - const frameworks = _.uniqBy(allDependencies, "framework") - .map((item) => item && item.framework) - .filter((item) => item); return frameworks; } diff --git a/lib/services/vitest-execution-service.ts b/lib/services/vitest-execution-service.ts new file mode 100644 index 0000000000..f4811780e8 --- /dev/null +++ b/lib/services/vitest-execution-service.ts @@ -0,0 +1,95 @@ +import * as path from "path"; +import { IProjectData, IVitestExecutionService } from "../definitions/project"; +import { IOptions } from "../declarations"; +import { IChildProcess, IErrors, IFileSystem } from "../common/declarations"; +import { injector } from "../common/yok"; +import { resolvePackagePath } from "../helpers/package-path-helper"; + +const VITEST_CONFIG_FILES = [ + "vitest.config.mts", + "vitest.config.ts", + "vitest.config.mjs", + "vitest.config.js", +]; + +export class VitestExecutionService implements IVitestExecutionService { + constructor( + private $childProcess: IChildProcess, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $options: IOptions, + ) {} + + public isVitestProject(projectData: IProjectData): boolean { + return !!this.getConfigPath(projectData); + } + + public canStartTestRun(projectData: IProjectData): boolean { + return ( + this.isVitestProject(projectData) && + !!resolvePackagePath("vitest", { paths: [projectData.projectDir] }) + ); + } + + public async startTestRun( + platform: string, + projectData: IProjectData, + ): Promise { + const vitestPackagePath = resolvePackagePath("vitest", { + paths: [projectData.projectDir], + }); + if (!vitestPackagePath) { + this.$errors.fail( + "Unable to find 'vitest' in the project. Run '$ ns test init --framework vitest' first.", + ); + } + + if (this.$options.watch) { + this.$logger.warn( + "'--watch' is not supported for on-device Vitest runs yet; running once.", + ); + } + + const args = [path.join(vitestPackagePath, "vitest.mjs"), "run"]; + if (this.$options.env && this.$options.env.codeCoverage) { + args.push("--coverage"); + } + + const env: NodeJS.ProcessEnv = { + ...process.env, + NS_PLATFORM: platform.toLowerCase(), + }; + if (this.$options.device) { + env.NS_DEVICE = this.$options.device; + } + + const result = await this.$childProcess.spawnFromEvent( + process.execPath, + args, + "close", + { + cwd: projectData.projectDir, + stdio: "inherit", + env, + }, + { throwError: false }, + ); + + if (result.exitCode !== 0) { + this.$errors.fail("Test run failed."); + } + } + + private getConfigPath(projectData: IProjectData): string { + for (const configFile of VITEST_CONFIG_FILES) { + const configPath = path.join(projectData.projectDir, configFile); + if (this.$fs.exists(configPath)) { + return configPath; + } + } + return null; + } +} + +injector.register("vitestExecutionService", VitestExecutionService); diff --git a/lib/services/xcconfig-service.ts b/lib/services/xcconfig-service.ts index 1b99ffa962..ff0acbffe7 100644 --- a/lib/services/xcconfig-service.ts +++ b/lib/services/xcconfig-service.ts @@ -10,16 +10,20 @@ import * as _ from "lodash"; import { injector } from "../common/yok"; export class XcconfigService implements IXcconfigService { - constructor(private $childProcess: IChildProcess, private $fs: IFileSystem) {} + private static readonly CONFLICT_MARKER = "NS_XCCONFIG_CONFLICTS:"; + + constructor( + private $childProcess: IChildProcess, + private $fs: IFileSystem, + private $logger: ILogger, + ) {} public getPluginsXcconfigFilePaths(projectRoot: string): IStringDictionary { return { - [Configurations.Debug.toLowerCase()]: this.getPluginsDebugXcconfigFilePath( - projectRoot - ), - [Configurations.Release.toLowerCase()]: this.getPluginsReleaseXcconfigFilePath( - projectRoot - ), + [Configurations.Debug.toLowerCase()]: + this.getPluginsDebugXcconfigFilePath(projectRoot), + [Configurations.Release.toLowerCase()]: + this.getPluginsReleaseXcconfigFilePath(projectRoot), }; } @@ -33,28 +37,95 @@ export class XcconfigService implements IXcconfigService { public async mergeFiles( sourceFile: string, - destinationFile: string + destinationFile: string, ): Promise { if (!this.$fs.exists(destinationFile)) { this.$fs.writeFile(destinationFile, ""); } - const escapedDestinationFile = destinationFile.replace(/'/g, "\\'"); - const escapedSourceFile = sourceFile.replace(/'/g, "\\'"); - - const mergeScript = `require 'xcodeproj'; - userConfig = Xcodeproj::Config.new('${escapedDestinationFile}') - existingConfig = Xcodeproj::Config.new('${escapedSourceFile}') - userConfig.attributes.each do |key,| - existingConfig.attributes.delete(key) if (userConfig.attributes.key?(key) && existingConfig.attributes.key?(key)) + // A key already present in the destination wins, so the incoming one is + // dropped, unless the kept value references $(inherited), which asks + // for the setting to be added to rather than replaced. Those accumulate, + // or a search path list such as HEADER_SEARCH_PATHS would keep only + // whichever file was merged first and the build would fail on headers + // the plugins and the pods do ship. The incoming marker is stripped so + // it stays at the front of the merged value. + // + // Report the drops whose values actually differ: a silently discarded + // setting is otherwise indistinguishable from one that was never + // written, which makes a plugin pinning e.g. + // CLANG_CXX_LANGUAGE_STANDARD very hard to track down. + // + // The paths are passed as argv rather than interpolated: they come from + // the project and node_modules layout, and a shell-interpolated command + // would execute anything a directory name expands to. + const mergeScript = `require 'xcodeproj' + require 'json' + destination, source = ARGV + inherited = /\\$[({]inherited[)}]/ + userConfig = Xcodeproj::Config.new(destination) + existingConfig = Xcodeproj::Config.new(source) + conflicts = [] + userConfig.attributes.each do |key, kept| + next unless existingConfig.attributes.key?(key) + incoming = existingConfig.attributes[key] + if kept.to_s =~ inherited + appended = incoming.to_s.gsub(inherited, '').strip + if appended.empty? + existingConfig.attributes.delete(key) + else + existingConfig.attributes[key] = appended + end + next + end + conflicts << { 'key' => key, 'kept' => kept.to_s, 'ignored' => incoming.to_s } if incoming.to_s != kept.to_s + existingConfig.attributes.delete(key) end - userConfig.merge(existingConfig).save_as(Pathname.new('${escapedDestinationFile}'))`; - await this.$childProcess.exec(`ruby -e "${mergeScript}"`); + userConfig.merge(existingConfig).save_as(Pathname.new(destination)) + print '${XcconfigService.CONFLICT_MARKER}' + JSON.generate(conflicts)`; + const output = await this.$childProcess.execFile("ruby", [ + "-e", + mergeScript, + destinationFile, + sourceFile, + ]); + this.warnAboutConflicts(sourceFile, output); + } + + private warnAboutConflicts(sourceFile: string, output: any): void { + const text: string = + output === null || output === undefined ? "" : `${output}`; + const markerIndex = text.lastIndexOf(XcconfigService.CONFLICT_MARKER); + if (markerIndex === -1) { + return; + } + + let conflicts: { key: string; kept: string; ignored: string }[]; + try { + conflicts = JSON.parse( + text.substring(markerIndex + XcconfigService.CONFLICT_MARKER.length), + ); + } catch (err) { + // Never let a reporting problem fail the merge itself. + this.$logger.trace( + `Unable to read xcconfig conflicts for ${sourceFile}: ${err}`, + ); + return; + } + + for (const conflict of conflicts || []) { + this.$logger.warn( + `Ignoring ${conflict.key} = ${conflict.ignored} from ${sourceFile}: ` + + `already set to ${conflict.kept} by a higher precedence xcconfig. ` + + `The app's App_Resources xcconfig is applied first, then each ` + + `plugin's in dependency order.`, + ); + } } public readPropertyValue( xcconfigFilePath: string, - propertyName: string + propertyName: string, ): string { if (this.$fs.exists(xcconfigFilePath)) { const text = this.$fs.readText(xcconfigFilePath); diff --git a/lib/sys-info.ts b/lib/sys-info.ts index 98a9a22692..8675df917b 100644 --- a/lib/sys-info.ts +++ b/lib/sys-info.ts @@ -16,14 +16,18 @@ import { ISystemWarning, } from "./common/declarations"; import { injector } from "./common/yok"; +import { SystemWarningsSeverity } from "./definitions/system-warnings"; export class SysInfo implements ISysInfo { private sysInfo: ISysInfoData = null; - constructor(private $fs: IFileSystem, private $hostInfo: IHostInfo) {} + constructor( + private $fs: IFileSystem, + private $hostInfo: IHostInfo, + ) {} public async getSysInfo( - config?: NativeScriptDoctor.ISysInfoConfig + config?: NativeScriptDoctor.ISysInfoConfig, ): Promise { if (!this.sysInfo) { const pathToNativeScriptCliPackageJson = diff --git a/lib/tools/config-manipulation/config-transformer.ts b/lib/tools/config-manipulation/config-transformer.ts index 50f926a530..0a752b3d2a 100644 --- a/lib/tools/config-manipulation/config-transformer.ts +++ b/lib/tools/config-manipulation/config-transformer.ts @@ -18,11 +18,7 @@ import { } from "ts-morph"; export type SupportedConfigValues = - | string - | number - | boolean - | { [key: string]: SupportedConfigValues } - | any[]; + string | number | boolean | { [key: string]: SupportedConfigValues } | any[]; export interface IConfigTransformer { /** @@ -68,7 +64,7 @@ export class ConfigTransformer implements IConfigTransformer { ).getExpressionIfKind(SyntaxKind.BinaryExpression); const leftSide = expression.getLeft() as PropertyAccessExpression; if (leftSide.getFullText().trim() === "module.exports") { - exportValue = expression.getRight(); + exportValue = this.unwrapObjectLiteral(expression.getRight()); return true; } } @@ -80,20 +76,39 @@ export class ConfigTransformer implements IConfigTransformer { const exports = this.config .getDefaultExportSymbolOrThrow() .getDeclarations()[0] as ExportAssignment; - const expr = exports.getExpression(); - exportValue = - expr.getChildCount() > 0 - ? (expr.getChildAtIndex(0) as ObjectLiteralExpression) - : expr; + exportValue = this.unwrapObjectLiteral(exports.getExpression()); } - if (!Node.isObjectLiteralExpression(exportValue)) { + if (!exportValue) { throw new Error("default export must be an object!"); } return exportValue; } + /** + * Strips the type assertions and parentheses a config may wrap its object in + * - `{...} as NativeScriptConfig`, `satisfies`, `{...}`, `({...})` and + * any nesting of those - none of which change the exported object. + * @returns the object literal, or undefined if the export is not one. + */ + private unwrapObjectLiteral(node: Node): ObjectLiteralExpression { + if (Node.isObjectLiteralExpression(node)) { + return node; + } + + if ( + Node.isParenthesizedExpression(node) || + Node.isAsExpression(node) || + Node.isSatisfiesExpression(node) || + Node.isTypeAssertion(node) + ) { + return this.unwrapObjectLiteral(node.getExpression()); + } + + return undefined; + } + private getProperty( key: string, parent: ObjectLiteralExpression = null, diff --git a/lib/tools/plist-merge/plist-session.ts b/lib/tools/plist-merge/plist-session.ts new file mode 100644 index 0000000000..8173fd58c4 --- /dev/null +++ b/lib/tools/plist-merge/plist-session.ts @@ -0,0 +1,141 @@ +// Inlined from the plist-merge-patch package (NativeScript, Apache-2.0), which +// was unmaintained and pinned older copies of plist and lodash than the CLI +// already depends on. +import * as plist from "plist"; +import * as _ from "lodash"; + +export interface Reporter { + log?(msg: string): void; + warn?(msg: string): void; +} + +export interface Patch { + name: string; + read(): string; +} + +export interface ICFBundleURLType { + CFBundleTypeRole: string; + CFBundleURLSchemes: string[]; +} + +const CF_BUNDLE_URL_TYPES = "CFBundleURLTypes"; +const LS_APPLICATION_QUERIES_SCHEMES = "LSApplicationQueriesSchemes"; + +export class PlistMerger { + constructor(private reporter?: Reporter) {} + + public merge(base: any, patch: any): any { + const baseClone = _.cloneDeep(base); + _.mergeWith(baseClone, patch, this.customizer.bind(this)); + + return baseClone; + } + + /** + * Entries declaring the same role are folded into one, so an app and its + * plugins can each contribute schemes to a role without displacing each + * other. Roles not already present are appended. + */ + private mergeCFBundleURLTypes( + baseValue: ICFBundleURLType[], + patchValue: ICFBundleURLType[], + ): ICFBundleURLType[] { + for (const patchElement of patchValue) { + let shouldAddToBase = true; + + for (const baseElement of baseValue) { + if (!patchElement.CFBundleTypeRole || !baseElement.CFBundleTypeRole) { + this.warn( + `Merging ${CF_BUNDLE_URL_TYPES}: Property CFBundleTypeRole is required!`, + ); + } + + if (patchElement.CFBundleTypeRole === baseElement.CFBundleTypeRole) { + baseElement.CFBundleURLSchemes = + baseElement.CFBundleURLSchemes.concat( + patchElement.CFBundleURLSchemes, + ); + shouldAddToBase = false; + } + } + + if (shouldAddToBase) { + baseValue.push(patchElement); + } + } + + return baseValue; + } + + private mergeLSApplicationQueriesSchemes( + baseValue: string[], + patchValue: string[], + ): string[] { + for (const patchElement of patchValue) { + if (!baseValue.some((x) => x === patchElement)) { + baseValue.push(patchElement); + } + } + + return baseValue; + } + + private customizer(baseValue: any, patchValue: any, key: string): any { + if (key === CF_BUNDLE_URL_TYPES && !!baseValue) { + return this.mergeCFBundleURLTypes(baseValue, patchValue); + } else if (key === LS_APPLICATION_QUERIES_SCHEMES && !!baseValue) { + return this.mergeLSApplicationQueriesSchemes(baseValue, patchValue); + } + + // every other array is replaced rather than concatenated, which is what + // lodash would otherwise do for two arrays + if (_.isArray(baseValue)) { + return patchValue; + } + } + + private warn(msg: string): void { + if (this.reporter && this.reporter.warn) { + this.reporter.warn(msg); + } + } +} + +export class PlistSession { + private patches: Patch[] = []; + + constructor(private reporter?: Reporter) {} + + public get hasPatches(): boolean { + return this.patches.length > 0; + } + + public patch(patch: Patch): void { + this.patches.push(patch); + } + + public build(): string { + this.log(`Start`); + + const plistMerger = new PlistMerger(this.reporter); + let jsonPlist: any = {}; + + for (const patch of this.patches) { + this.log(`Patch '${patch.name}'`); + const patchJson = plist.parse(patch.read()); + jsonPlist = plistMerger.merge(jsonPlist, patchJson); + } + + const resultString = plist.build(jsonPlist); + this.log(`Complete`); + + return resultString; + } + + private log(msg: string): void { + if (this.reporter && this.reporter.log) { + this.reporter.log(msg); + } + } +} diff --git a/package-lock.json b/package-lock.json index a4e4386333..b8c9b3872e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,21 @@ { "name": "nativescript", - "version": "9.0.6", + "version": "9.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nativescript", - "version": "9.0.6", + "version": "9.1.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@foxt/js-srp": "0.0.3-patch2", - "@nativescript/doctor": "2.0.17", - "@nativescript/hook": "3.0.4", + "@nativescript/doctor": "2.0.18", + "@nativescript/hook": "3.0.5", "@npmcli/arborist": "9.1.8", - "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", - "axios": "1.13.5", + "axios": "1.18.1", "byline": "5.0.0", "chokidar": "^3.6.0", "cli-table3": "0.6.5", @@ -24,27 +23,25 @@ "convert-source-map": "2.0.0", "detect-newline": "3.1.0", "email-validator": "2.0.4", - "esprima": "4.0.1", "font-finder": "1.1.0", - "ios-device-lib": "0.9.4", + "ios-device-lib": "0.9.5", "ios-mobileprovision-finder": "1.2.1", "ios-sim-portable": "4.5.1", - "jimp": "1.6.0", - "lodash": "4.17.23", + "jimp": "1.6.1", + "lodash": "4.18.1", "log4js": "6.9.1", "marked": "15.0.12", "marked-terminal": "7.3.0", - "minimatch": "10.2.4", + "minimatch": "10.2.6", "mkdirp": "3.0.1", "mute-stream": "2.0.0", - "nativescript-dev-xcode": "0.8.1", + "nativescript-dev-xcode": "0.8.2", "open": "8.4.2", "ora": "5.4.1", "pacote": "21.0.4", "pbxproj-dom": "1.2.0", "plist": "3.1.0", - "plist-merge-patch": "0.2.0", - "prettier": "3.7.3", + "prettier": "3.9.6", "prompts": "2.4.2", "proper-lockfile": "4.1.2", "proxy-lib": "0.4.1", @@ -52,17 +49,16 @@ "qrcode-terminal": "0.12.0", "semver": "7.7.3", "shelljs": "0.10.0", - "simple-git": "3.30.0", + "simple-git": "3.36.0", "simple-plist": "1.4.0", - "source-map": "0.7.6", - "tar": "7.5.9", - "ts-morph": "25.0.1", + "source-map": "0.8.0", + "tar": "7.5.22", + "ts-morph": "28.0.0", "tunnel": "0.0.6", "typescript": "5.7.3", - "universal-analytics": "0.5.3", - "uuid": "11.1.0", + "uuid": "11.1.1", "winreg": "1.2.5", - "ws": "8.18.3", + "ws": "8.21.1", "xml2js": "0.6.2", "yargs": "17.7.2", "yazl": "^3.3.1" @@ -91,11 +87,10 @@ "@types/retry": "0.12.5", "@types/semver": "7.7.1", "@types/shelljs": "^0.8.11", - "@types/sinon": "^17.0.3", + "@types/sinon": "^22.0.0", "@types/tabtab": "^3.0.2", - "@types/tar": "6.1.13", + "@types/tar": "7.0.87", "@types/tunnel": "0.0.7", - "@types/universal-analytics": "0.4.8", "@types/uuid": "^10.0.0", "@types/ws": "8.18.1", "@types/xml2js": "0.4.14", @@ -105,20 +100,13 @@ "chai": "5.3.3", "chai-as-promised": "8.0.2", "conventional-changelog-cli": "^5.0.0", + "dotenv": "17.4.2", "fast-check": "3.23.2", - "grunt": "1.6.1", - "grunt-contrib-clean": "2.0.1", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-watch": "1.1.0", - "grunt-shell": "4.0.0", - "grunt-template": "1.0.0", - "grunt-ts": "6.0.0-beta.22", "husky": "9.1.7", - "istanbul": "0.4.5", "lint-staged": "~15.5.2", - "mocha": "11.7.5", - "sinon": "19.0.5", + "sinon": "22.1.0", "source-map-support": "0.5.21", + "vitest": "^4.1.10", "xml2js": ">=0.5.0" }, "engines": { @@ -153,6 +141,16 @@ "node": ">=6.9.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -164,14 +162,14 @@ } }, "node_modules/@conventional-changelog/git-client": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.5.1.tgz", - "integrity": "sha512-lAw7iA5oTPWOLjiweb7DlGEMDEvzqzLLa6aWOly2FSZ64IwLE8T458rC+o+WvI31Doz6joM7X2DoNog7mX8r4A==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", + "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", "dev": true, "license": "MIT", "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.1.0", + "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "engines": { @@ -179,7 +177,7 @@ }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.1.0" + "conventional-commits-parser": "^6.4.0" }, "peerDependenciesMeta": { "conventional-commits-filter": { @@ -190,16 +188,38 @@ } } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@foxt/js-srp": { @@ -230,120 +250,11 @@ "node": ">=10.13.0" } }, - "node_modules/@ionic/utils-array": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", - "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", - "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-fs": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", - "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^8.0.0", - "debug": "^4.0.0", - "fs-extra": "^9.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-object": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", - "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", - "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-process": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", - "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", - "license": "MIT", - "dependencies": { - "@ionic/utils-object": "2.1.6", - "@ionic/utils-terminal": "2.3.4", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "tree-kill": "^1.2.2", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", - "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", - "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-subprocess": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.14.tgz", - "integrity": "sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==", - "license": "MIT", - "dependencies": { - "@ionic/utils-array": "2.1.6", - "@ionic/utils-fs": "3.1.7", - "@ionic/utils-process": "2.1.11", - "@ionic/utils-stream": "3.1.6", - "@ionic/utils-terminal": "2.3.4", - "cross-spawn": "^7.0.3", - "debug": "^4.0.0", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ionic/utils-terminal": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", - "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", - "license": "MIT", - "dependencies": { - "@types/slice-ansi": "^4.0.0", - "debug": "^4.0.0", - "signal-exit": "^3.0.3", - "slice-ansi": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "tslib": "^2.0.1", - "untildify": "^4.0.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -368,17 +279,17 @@ "license": "ISC" }, "node_modules/@jimp/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.0.tgz", - "integrity": "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz", + "integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==", "license": "MIT", "dependencies": { - "@jimp/file-ops": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/file-ops": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", - "file-type": "^16.0.0", + "file-type": "^21.3.3", "mime": "3" }, "engines": { @@ -386,14 +297,14 @@ } }, "node_modules/@jimp/diff": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.0.tgz", - "integrity": "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz", + "integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==", "license": "MIT", "dependencies": { - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" }, "engines": { @@ -401,23 +312,23 @@ } }, "node_modules/@jimp/file-ops": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.0.tgz", - "integrity": "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz", + "integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@jimp/js-bmp": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.0.tgz", - "integrity": "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz", + "integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "bmp-ts": "^1.0.9" }, "engines": { @@ -425,13 +336,13 @@ } }, "node_modules/@jimp/js-gif": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.0.tgz", - "integrity": "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz", + "integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "gifwrap": "^0.10.1", "omggif": "^1.0.10" }, @@ -440,13 +351,13 @@ } }, "node_modules/@jimp/js-jpeg": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.0.tgz", - "integrity": "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz", + "integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "jpeg-js": "^0.4.4" }, "engines": { @@ -454,13 +365,13 @@ } }, "node_modules/@jimp/js-png": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.0.tgz", - "integrity": "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz", + "integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "pngjs": "^7.0.0" }, "engines": { @@ -468,13 +379,13 @@ } }, "node_modules/@jimp/js-tiff": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.0.tgz", - "integrity": "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz", + "integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "utif2": "^4.1.0" }, "engines": { @@ -482,13 +393,13 @@ } }, "node_modules/@jimp/plugin-blit": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.0.tgz", - "integrity": "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz", + "integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -496,25 +407,25 @@ } }, "node_modules/@jimp/plugin-blur": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.0.tgz", - "integrity": "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz", + "integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/utils": "1.6.0" + "@jimp/core": "1.6.1", + "@jimp/utils": "1.6.1" }, "engines": { "node": ">=18" } }, "node_modules/@jimp/plugin-circle": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.0.tgz", - "integrity": "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz", + "integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -522,14 +433,14 @@ } }, "node_modules/@jimp/plugin-color": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.0.tgz", - "integrity": "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz", + "integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "tinycolor2": "^1.6.0", "zod": "^3.23.8" }, @@ -538,16 +449,16 @@ } }, "node_modules/@jimp/plugin-contain": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.0.tgz", - "integrity": "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz", + "integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -555,15 +466,15 @@ } }, "node_modules/@jimp/plugin-cover": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.0.tgz", - "integrity": "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz", + "integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -571,14 +482,14 @@ } }, "node_modules/@jimp/plugin-crop": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.0.tgz", - "integrity": "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz", + "integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -586,13 +497,13 @@ } }, "node_modules/@jimp/plugin-displace": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.0.tgz", - "integrity": "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz", + "integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -600,25 +511,25 @@ } }, "node_modules/@jimp/plugin-dither": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.0.tgz", - "integrity": "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz", + "integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0" + "@jimp/types": "1.6.1" }, "engines": { "node": ">=18" } }, "node_modules/@jimp/plugin-fisheye": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.0.tgz", - "integrity": "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz", + "integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -626,12 +537,12 @@ } }, "node_modules/@jimp/plugin-flip": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.0.tgz", - "integrity": "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz", + "integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -639,20 +550,20 @@ } }, "node_modules/@jimp/plugin-hash": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.0.tgz", - "integrity": "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==", - "license": "MIT", - "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/js-bmp": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/js-tiff": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz", + "integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "any-base": "^1.1.0" }, "engines": { @@ -660,12 +571,12 @@ } }, "node_modules/@jimp/plugin-mask": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.0.tgz", - "integrity": "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz", + "integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -673,16 +584,16 @@ } }, "node_modules/@jimp/plugin-print": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.0.tgz", - "integrity": "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz", + "integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/types": "1.6.1", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", @@ -694,9 +605,9 @@ } }, "node_modules/@jimp/plugin-quantize": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.0.tgz", - "integrity": "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz", + "integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==", "license": "MIT", "dependencies": { "image-q": "^4.0.0", @@ -707,13 +618,13 @@ } }, "node_modules/@jimp/plugin-resize": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz", - "integrity": "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz", + "integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/types": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/types": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -721,16 +632,16 @@ } }, "node_modules/@jimp/plugin-rotate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.0.tgz", - "integrity": "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz", + "integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -738,16 +649,16 @@ } }, "node_modules/@jimp/plugin-threshold": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.0.tgz", - "integrity": "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz", + "integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==", "license": "MIT", "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-hash": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.0", + "@jimp/core": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1", "zod": "^3.23.8" }, "engines": { @@ -755,9 +666,9 @@ } }, "node_modules/@jimp/types": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.0.tgz", - "integrity": "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz", + "integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==", "license": "MIT", "dependencies": { "zod": "^3.23.8" @@ -767,43 +678,25 @@ } }, "node_modules/@jimp/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==", "license": "MIT", "dependencies": { - "@jimp/types": "1.6.0", + "@jimp/types": "1.6.1", "tinycolor2": "^1.6.0" }, "engines": { "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "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==", + "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -819,46 +712,43 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nativescript/doctor": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@nativescript/doctor/-/doctor-2.0.17.tgz", - "integrity": "sha512-+S3nL9/OwsrQ75MkolXtLYOuzzjc2W9EEqys7hg8FLFJbcQc47TYmpGLf/v7bdwx6Rh8G3HTcKy0QSkjInL1WQ==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@nativescript/doctor/-/doctor-2.0.18.tgz", + "integrity": "sha512-ir8IqN5IQa8UTYq4Xr+58A0HmqBPDeu2wWiMXctT7q9SPa3XcBVmZLGgO7alPf+unP4jVhlz0Rvg+Ztruiw/Eg==", "license": "Apache-2.0", "dependencies": { - "lodash": "4.17.21", - "semver": "7.7.2", + "lodash": "^4.18.1", + "semver": "7.7.3", "shelljs": "0.10.0", "winreg": "1.2.5", - "yauzl": "3.2.0" - } - }, - "node_modules/@nativescript/doctor/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/@nativescript/doctor/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "yauzl": "^3.4.0" } }, "node_modules/@nativescript/hook": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.4.tgz", - "integrity": "sha512-oahiN7V0D+fgl9o8mjGRgExujTpgSBB0DAFr3eX91qdlJZV8ywJ6mnvtHZyEI2j46yPgAE8jmNIw/Z/d3aWetw==", - "license": "Apache-2.0", - "dependencies": { - "glob": "^11.0.0", - "mkdirp": "^3.0.1" - } + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.5.tgz", + "integrity": "sha512-MzL7R/nPZU2qnvDWWuJ8RB7H3luEwANgFX/d/ILdg7bSYxl6uANCfzlueHnWgrQmBxu6dTkvPsFW2WNVUxlhUg==", + "license": "Apache-2.0" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1162,79 +1052,14 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@nstudio/trapezedev-project": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@nstudio/trapezedev-project/-/trapezedev-project-7.2.3.tgz", - "integrity": "sha512-EIxEGjwPeMfBVkxvRvf8GY+pD3lfu8CP2sh/AD5eV4fgW/gpgRYE9WKbW4QJLj7WIRgQbn6Oos9NXq1OfTmzhA==", - "license": "SEE LICENSE", - "dependencies": { - "@ionic/utils-fs": "^3.1.5", - "@ionic/utils-subprocess": "^2.1.8", - "@prettier/plugin-xml": "^2.2.0", - "@trapezedev/gradle-parse": "7.1.3", - "@xmldom/xmldom": "^0.8.11", - "cross-spawn": "^7.0.3", - "diff": "^5.1.0", - "env-paths": "^3.0.0", - "gradle-to-js": "^2.0.0", - "ini": "^2.0.0", - "kleur": "^4.1.5", - "lodash": "^4.17.21", - "plist": "^3.0.4", - "prettier": "^2.7.1", - "prompts": "^2.4.2", - "replace": "^1.1.0", - "tmp": "^0.2.1", - "ts-node": "^10.2.1", - "xcode": "^3.0.1", - "xml-js": "^1.6.11", - "xpath": "^0.0.32", - "yargs": "^17.2.1" - } - }, - "node_modules/@nstudio/trapezedev-project/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/@nstudio/trapezedev-project/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "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==", + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@prettier/plugin-xml": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@prettier/plugin-xml/-/plugin-xml-2.2.0.tgz", - "integrity": "sha512-UWRmygBsyj4bVXvDiqSccwT1kmsorcwQwaIy30yVh8T+Gspx4OlC0shX1y+ZuwXZvgnafmpRYKks0bAu9urJew==", - "license": "MIT", - "dependencies": { - "@xml-tools/parser": "^1.0.11", - "prettier": ">=2.4.0" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@rigor789/resolve-package-path": { @@ -1243,57 +1068,321 @@ "integrity": "sha512-/JqGCvHpj0PxS9cyZPP5LpiEy1pYszgWor/JTyreHQwLPQQdxO1mUYTtRribYcVosxH7FFs0GJBtJ652nlwKyw==", "license": "MIT" }, - "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", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" - }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^20.19.0 || >=22.12.0" } }, - "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", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^20.19.0 || >=22.12.0" } }, - "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", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.19.0 || >=22.12.0" } }, - "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", - "dependencies": { + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "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", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.3", - "proc-log": "^6.1.0", - "promise-retry": "^2.0.1" + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "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==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", @@ -1304,28 +1393,42 @@ } }, "node_modules/@sigstore/verify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", - "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.1.tgz", - "integrity": "sha512-3nWd8irxvDI6v856wpPCHZ+08iQR0oHTZfzAZmnbsLzf+Sf1odraP6uKOHDZToXq3RPRV/LbqGVlSCogm9cJjg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", + "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", "dev": true, "license": "MIT", "dependencies": { - "@simple-libs/stream-utils": "^1.1.0", - "@types/node": "^22.0.0" + "@simple-libs/stream-utils": "^1.2.0" }, "engines": { "node": ">=18" @@ -1335,14 +1438,11 @@ } }, "node_modules/@simple-libs/stream-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.1.0.tgz", - "integrity": "sha512-6rsHTjodIn/t90lv5snQjRPVtOosM7Vp0AKdrObymq45ojlgVwnpAqdc+0OBBrpEiy31zZ6/TKeIVqV1HwvnuQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "^22.0.0" - }, "engines": { "node": ">=18" }, @@ -1373,9 +1473,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1383,9 +1483,9 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1403,89 +1503,46 @@ "node": ">=4" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, - "node_modules/@trapezedev/gradle-parse": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@trapezedev/gradle-parse/-/gradle-parse-7.1.3.tgz", - "integrity": "sha512-WQVF5pEJ5o/mUyvfGTG9nBKx9Te/ilKM3r2IT69GlbaooItT5ao7RyF1MUTBNjHLPk/xpGUY3c6PyVnjDlz0Vw==", - "license": "SEE LICENSE" - }, - "node_modules/@ts-morph/common": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.26.1.tgz", - "integrity": "sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.3.2", - "minimatch": "^9.0.4", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/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==", "license": "MIT" }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "debug": "^4.4.3", + "token-types": "^6.1.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "license": "MIT" + "node_modules/@ts-morph/common": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz", + "integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", @@ -1509,6 +1566,17 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/byline": { "version": "4.2.36", "resolved": "https://registry.npmjs.org/@types/byline/-/byline-4.2.36.tgz", @@ -1598,14 +1666,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/fs-extra": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", - "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/lodash": { "version": "4.17.23", @@ -1641,9 +1707,10 @@ } }, "node_modules/@types/node": { - "version": "22.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", - "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1810,9 +1877,9 @@ } }, "node_modules/@types/sinon": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", - "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1826,12 +1893,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", - "license": "MIT" - }, "node_modules/@types/ssri": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.5.tgz", @@ -1853,24 +1914,14 @@ } }, "node_modules/@types/tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "version": "7.0.87", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-7.0.87.tgz", + "integrity": "sha512-3IxNBV8LeY5oi2ZFpvAhOtW1+mHswkzM7BuisVrwJgPv67GBO2rkLPQlEKtzfHuLdhDDczhkCZeT+RuizMay4A==", + "deprecated": "This is a stub types definition. tar provides its own type definitions, so you do not need this installed.", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "minipass": "^4.0.0" - } - }, - "node_modules/@types/tar/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" + "tar": "*" } }, "node_modules/@types/tunnel": { @@ -1883,13 +1934,6 @@ "@types/node": "*" } }, - "node_modules/@types/universal-analytics": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@types/universal-analytics/-/universal-analytics-0.4.8.tgz", - "integrity": "sha512-HozCrji3dIImmQcKnP7cN0ZBiYTjuOavzgPRY0CbT4AQ2zH/ZRqYDNTMiYI7aBeMV5ylbu+h59WG/N8qGePmww==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -1944,65 +1988,136 @@ "@types/node": "*" } }, - "node_modules/@xml-tools/parser": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", - "integrity": "sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==", - "license": "Apache-2.0", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", "dependencies": { - "chevrotain": "7.1.1" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "node_modules/@vitest/expect/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=18" } }, - "node_modules/abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=6.5" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">=10.0.0" } }, "node_modules/add-stream": { @@ -2021,17 +2136,6 @@ "node": ">= 14" } }, - "node_modules/amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", - "dev": true, - "license": "BSD-3-Clause OR MIT", - "optional": true, - "engines": { - "node": ">=0.4.2" - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -2099,62 +2203,6 @@ "node": ">= 8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -2162,26 +2210,6 @@ "dev": true, "license": "MIT" }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2192,73 +2220,12 @@ "node": ">=12" } }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/await-to-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", @@ -2269,69 +2236,49 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "debug": "4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6.0.0" } }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "is-descriptor": "^1.0.0" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": "18 || 20 || >=22" } }, "node_modules/base64-js": { @@ -2391,17 +2338,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2433,18 +2369,6 @@ "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", "license": "MIT" }, - "node_modules/body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==", - "dev": true, - "dependencies": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, "node_modules/bplist-creator": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.1.tgz", @@ -2467,15 +2391,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -2490,13 +2414,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2554,12 +2471,6 @@ "node": ">=0.10.0" } }, - "node_modules/bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", - "dev": true - }, "node_modules/cacache": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", @@ -2599,27 +2510,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.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", @@ -2633,36 +2523,6 @@ "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==", - "dev": true, - "license": "MIT", - "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/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2724,15 +2584,6 @@ "node": ">= 16" } }, - "node_modules/chevrotain": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.1.1.tgz", - "integrity": "sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==", - "license": "Apache-2.0", - "dependencies": { - "regexp-to-ast": "0.5.0" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2766,22 +2617,6 @@ "node": ">=18" } }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -3060,20 +2895,6 @@ "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT" }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -3122,16 +2943,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3171,32 +2982,10 @@ "dot-prop": "^5.1.0" } }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==", - "dev": true - }, - "node_modules/conventional-changelog": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-6.0.0.tgz", - "integrity": "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w==", + "node_modules/conventional-changelog": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-6.0.0.tgz", + "integrity": "sha512-tuUH8H/19VjtD9Ig7l6TQRh+Z0Yt0NZ6w/cCkkyzUbGQTnUEmKfGtkC9gGfVgCfOL1Rzno5NgNF4KY8vR+Jo3w==", "dev": true, "license": "MIT", "dependencies": { @@ -3416,12 +3205,13 @@ } }, "node_modules/conventional-commits-parser": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.1.tgz", - "integrity": "sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", + "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", "dev": true, "license": "MIT", "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", "meow": "^13.0.0" }, "bin": { @@ -3437,29 +3227,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3495,60 +3262,6 @@ "node": ">= 8" } }, - "node_modules/csproj2ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-1.1.0.tgz", - "integrity": "sha512-sk0RTT51t4lUNQ7UfZrqjQx7q4g0m3iwNA6mvyh7gLsgQYvwKzfdyoAgicC9GqJvkoIkU0UmndV9c7VZ8pJ45Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^4.1.1", - "lodash": "^4.17.4", - "semver": "^5.4.1", - "xml2js": "^0.4.19" - } - }, - "node_modules/csproj2ts/node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/csproj2ts/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/csproj2ts/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/csproj2ts/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3570,16 +3283,6 @@ "node": ">=4.0" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3597,29 +3300,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3630,13 +3310,6 @@ "node": ">=6" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3658,19 +3331,6 @@ "node": ">=8" } }, - "node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3680,27 +3340,14 @@ "node": ">=0.4.0" } }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", - "dependencies": { - "repeating": "^2.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -3713,9 +3360,10 @@ } }, "node_modules/diff": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", - "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -3734,6 +3382,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3748,13 +3409,6 @@ "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, - "license": "MIT" - }, "node_modules/email-validator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", @@ -3784,18 +3438,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -3814,15 +3456,6 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, - "node_modules/error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", - "dev": true, - "dependencies": { - "string-template": "~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", @@ -3841,6 +3474,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3868,13 +3508,6 @@ "node": ">= 0.4" } }, - "node_modules/es6-promise": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", - "integrity": "sha512-FkHS6f1w/2Nj2kO8NsnLj2ZuCvcXHEMhZfmZSIBtY+DY2mPDDWxnSLG9CyygFW0hrb5RhOXVOvHpEUHS/6nkhQ==", - "dev": true, - "license": "MIT" - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3884,117 +3517,16 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=0.12.0" - }, - "optionalDependencies": { - "source-map": "~0.2.0" - } - }, - "node_modules/escodegen/node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", - "dev": true, - "optional": true, "dependencies": { - "amdefine": ">=0.0.4" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" + "@types/estree": "^1.0.0" } }, - "node_modules/eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", - "dev": true, - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -4002,15 +3534,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4095,164 +3618,49 @@ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.8.0" + "node": ">=12.0.0" } }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "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" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT", "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "pure-rand": "^6.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.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" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "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==", + "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", @@ -4265,13 +3673,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -4281,51 +3682,24 @@ "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/file-sync-cmp": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", - "integrity": "sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA==", - "dev": true, - "license": "MIT" - }, "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "license": "MIT", "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4338,23 +3712,6 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/find-up-simple": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", @@ -4368,69 +3725,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/findup-sync": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", - "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.3", - "micromatch": "^4.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -4460,33 +3764,11 @@ "node": ">8.0.0" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4503,6 +3785,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4512,49 +3795,21 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" } }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -4567,13 +3822,6 @@ "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==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4597,19 +3845,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "globule": "^1.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4691,25 +3926,6 @@ "node": ">8.0.0" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getobject": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", - "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/gifwrap": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", @@ -4721,13 +3937,14 @@ } }, "node_modules/git-raw-commits": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.0.tgz", - "integrity": "sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", + "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", + "deprecated": "Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.", "dev": true, "license": "MIT", "dependencies": { - "@conventional-changelog/git-client": "^1.0.0", + "@conventional-changelog/git-client": "^2.6.0", "meow": "^13.0.0" }, "bin": { @@ -4738,13 +3955,14 @@ } }, "node_modules/git-semver-tags": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-8.0.0.tgz", - "integrity": "sha512-N7YRIklvPH3wYWAR2vysaqGLPRcpwQ0GKdlqTiVN5w1UmCdaeY3K8s6DMKRCh54DDdzyt/OAB6C8jgVtb7Y2Fg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-8.0.1.tgz", + "integrity": "sha512-zMbamckSNdlT4U48IMFa2Cn6FTzM+2yF6/gEmStPJI8PiLxd/bT6dw10+mc6u5Qe4fhrc/y9nU290FWjQhAV7g==", + "deprecated": "Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.", "dev": true, "license": "MIT", "dependencies": { - "@conventional-changelog/git-client": "^1.0.0", + "@conventional-changelog/git-client": "^2.6.0", "meow": "^13.0.0" }, "bin": { @@ -4759,6 +3977,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", @@ -4790,138 +4009,76 @@ "node": ">= 6" } }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, + "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", - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/global-prefix/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "ISC" - }, - "node_modules/global-prefix/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/globule/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/globule/node_modules/brace-expansion": { - "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, + "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", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/globule/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globule/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "has-symbols": "^1.0.3" }, - "engines": { - "node": "*" - } - }, - "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", "engines": { "node": ">= 0.4" }, @@ -4929,2430 +4086,2278 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gradle-to-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/gradle-to-js/-/gradle-to-js-2.0.1.tgz", - "integrity": "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw==", - "license": "Apache-2.0", - "dependencies": { - "lodash.merge": "^4.6.2" - }, - "bin": { - "gradle-to-js": "cli.js" - } - }, - "node_modules/grunt": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", - "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { - "dateformat": "~4.6.2", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~5.0.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.3", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.6.3", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" - }, - "bin": { - "grunt": "bin/grunt" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=16" + "node": ">= 0.4" } }, - "node_modules/grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "bin": { - "grunt": "bin/grunt" - }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "engines": { - "node": ">=10" + "node": "*" } }, - "node_modules/grunt-cli/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, + "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", "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" + "lru-cache": "^11.1.0" }, - "bin": { - "nopt": "bin/nopt.js" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/grunt-contrib-clean": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz", - "integrity": "sha512-uRvnXfhiZt8akb/ZRDHJpQQtkkVkqc/opWO4Po/9ehC2hPxgptB9S6JHDC/Nxswo4CJSM0iFPT/Iym3cEMWzKA==", - "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" + }, + "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", "dependencies": { - "async": "^3.2.3", - "rimraf": "^2.6.2" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "grunt": ">=0.4.5" + "node": ">= 14" } }, - "node_modules/grunt-contrib-copy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", - "integrity": "sha512-gFRFUB0ZbLcjKb67Magz1yOHGBkyU6uL29hiEW1tdQ9gQt72NuMKIy/kS6dsCbV0cZ0maNCb0s6y+uT1FKU7jA==", - "dev": true, + "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", "dependencies": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 14" } }, - "node_modules/grunt-contrib-copy/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": ">=16.17.0" } }, - "node_modules/grunt-contrib-copy/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", + "bin": { + "husky": "bin.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" } }, - "node_modules/grunt-contrib-copy/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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", "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "minimatch": "^10.0.3" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/grunt-contrib-copy/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/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", "license": "MIT", - "engines": { - "node": ">=0.8.0" + "dependencies": { + "@types/node": "16.9.1" } }, - "node_modules/grunt-contrib-copy/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.8.19" } }, - "node_modules/grunt-contrib-copy/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" - }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "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", "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/grunt-contrib-watch/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "license": "MIT", + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", + "license": "MIT" + }, + "node_modules/ios-device-lib": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.9.5.tgz", + "integrity": "sha512-abZn5jjIDKG+P7K9fukdWTwDdSp8U9FhjIl2/qVa7VtKA0d8hm075tdi3IEzxClKAdpqv/0zO0b+wXBF3WVzkQ==", + "license": "Apache-2.0", "dependencies": { - "lodash": "^4.17.14" + "bufferpack": "0.0.6", + "uuid": "^11.1.0" } }, - "node_modules/grunt-known-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", - "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "license": "MIT", + "node_modules/ios-mobileprovision-finder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ios-mobileprovision-finder/-/ios-mobileprovision-finder-1.2.1.tgz", + "integrity": "sha512-9g1AJFRjWdCVdNKn7FpGv/t3EK5wFqP6EEhFVdUk7Pr8n4cVkFGyKa+l/XQdfjVUN5PnLHj5MY3x31Q3X8r80g==", + "license": "Apache-2.0", "dependencies": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" + "chalk": "^5.2.0", + "plist": "^3.0.6", + "yargs": "^17.7.1" }, - "engines": { - "node": ">= 0.10.0" + "bin": { + "ios-mobileprovision-finder": "src/ios-mobileprovision-finder.js" } }, - "node_modules/grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "license": "MIT", + "node_modules/ios-sim-portable": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/ios-sim-portable/-/ios-sim-portable-4.5.1.tgz", + "integrity": "sha512-g7qWaHXiFw7MuMrbwSQjeBpPl5zoHyhA6xArT2yhYg3U+WsoEy6d7x1Mgf6J77B+tvBJDWMjf2fjpuRBbQZggA==", + "license": "Apache-2.0", "dependencies": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" + "bplist-parser": "0.3.2", + "lodash": "4.17.21", + "plist": "3.0.6", + "shelljs": "~0.9.2", + "yargs": "17.7.1" + }, + "bin": { + "ios-sim-portable": "bin/ios-sim-portable.js", + "isim": "bin/ios-sim-portable.js" }, "engines": { - "node": ">=10" + "node": ">=6.0.0" } }, - "node_modules/grunt-legacy-log-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4.8" } }, - "node_modules/grunt-legacy-log-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/grunt-legacy-log-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "pump": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "license": "MIT", - "dependencies": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/grunt-legacy-util/node_modules/isexe": { + "node_modules/ios-sim-portable/node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, - "node_modules/grunt-legacy-util/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, + "node_modules/ios-sim-portable/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/ios-sim-portable/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=4" } }, - "node_modules/grunt-shell": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-4.0.0.tgz", - "integrity": "sha512-dHFy8VZDfWGYLTeNvIHze4PKXGvIlDWuN0UE7hUZstTQeiEyv1VmW1MaDYQ3X5tE3bCi3bEia1gGKH8z/f1czQ==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/plist": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", "license": "MIT", "dependencies": { - "chalk": "^3.0.0", - "npm-run-path": "^2.0.0", - "strip-ansi": "^6.0.1" + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "grunt": ">=1" + "node": ">=6" } }, - "node_modules/grunt-shell/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", + "node_modules/ios-sim-portable/node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "resolve": "^1.1.6" }, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/grunt-shell/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/ios-sim-portable/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" } }, - "node_modules/grunt-shell/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "shebang-regex": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/grunt-template": { + "node_modules/ios-sim-portable/node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-template/-/grunt-template-1.0.0.tgz", - "integrity": "sha512-1boA7QTNwnc8B3Wp4NEU0v99wrkLVXcgR0TC+M1fdstZs0Do8zRWuB5nY1Z9vH85vew9faUR2S0MhnWoymY25A==", - "dev": true, + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "license": "MIT", - "peerDependencies": { - "grunt": ">=0.4.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/grunt-ts": { - "version": "6.0.0-beta.22", - "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.22.tgz", - "integrity": "sha512-g9e+ZImQ7W38dfpwhp0+GUltXWidy3YGPfIA/IyGL5HMv6wmVmMMoSgscI5swhs2HSPf8yAvXAAJbwrouijoRg==", - "dev": true, - "license": "MIT", + "node_modules/ios-sim-portable/node_modules/shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "license": "BSD-3-Clause", "dependencies": { - "chokidar": "^2.0.4", - "csproj2ts": "^1.1.0", - "detect-indent": "^4.0.0", - "detect-newline": "^2.1.0", - "es6-promise": "~0.1.1", - "jsmin2": "^1.2.1", - "lodash": "~4.17.10", - "ncp": "0.5.1", - "rimraf": "2.2.6", - "semver": "^5.3.0", - "strip-bom": "^2.0.0" + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" }, - "engines": { - "node": ">= 0.8.0" + "bin": { + "shjs": "bin/shjs" }, - "peerDependencies": { - "grunt": "^1.0.0 || ^0.4.0", - "typescript": ">=1" + "engines": { + "node": ">=18" } }, - "node_modules/grunt-ts/node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "license": "ISC", "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "node_modules/grunt-ts/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, + "node_modules/ios-sim-portable/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "license": "MIT", "dependencies": { - "remove-trailing-separator": "^1.0.1" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/grunt-ts/node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, - "node_modules/grunt-ts/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/grunt-ts/node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" + "bin": { + "is-docker": "cli.js" }, - "optionalDependencies": { - "fsevents": "^1.2.7" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/grunt-ts/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/grunt-ts/node_modules/detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", - "dev": true, + "node_modules/is-fullwidth-code-point": { + "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==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/grunt-ts/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/grunt-ts/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "dev": true, - "hasInstallScript": true, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, "engines": { - "node": ">= 4.0" - } - }, - "node_modules/grunt-ts/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "node": ">=8" } }, - "node_modules/grunt-ts/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.0" } }, - "node_modules/grunt-ts/node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true, "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/grunt-ts/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/grunt-ts/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/grunt-ts/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "is-docker": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/grunt-ts/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.10.0" + "node": ">=20" } }, - "node_modules/grunt-ts/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/grunt-ts/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "node_modules/jimp": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz", + "integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.1", + "@jimp/diff": "1.6.1", + "@jimp/js-bmp": "1.6.1", + "@jimp/js-gif": "1.6.1", + "@jimp/js-jpeg": "1.6.1", + "@jimp/js-png": "1.6.1", + "@jimp/js-tiff": "1.6.1", + "@jimp/plugin-blit": "1.6.1", + "@jimp/plugin-blur": "1.6.1", + "@jimp/plugin-circle": "1.6.1", + "@jimp/plugin-color": "1.6.1", + "@jimp/plugin-contain": "1.6.1", + "@jimp/plugin-cover": "1.6.1", + "@jimp/plugin-crop": "1.6.1", + "@jimp/plugin-displace": "1.6.1", + "@jimp/plugin-dither": "1.6.1", + "@jimp/plugin-fisheye": "1.6.1", + "@jimp/plugin-flip": "1.6.1", + "@jimp/plugin-hash": "1.6.1", + "@jimp/plugin-mask": "1.6.1", + "@jimp/plugin-print": "1.6.1", + "@jimp/plugin-quantize": "1.6.1", + "@jimp/plugin-resize": "1.6.1", + "@jimp/plugin-rotate": "1.6.1", + "@jimp/plugin-threshold": "1.6.1", + "@jimp/types": "1.6.1", + "@jimp/utils": "1.6.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/grunt-ts/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" }, - "node_modules/grunt-ts/node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "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, + "license": "MIT" + }, + "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", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, "engines": { - "node": ">=0.10" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/grunt-ts/node_modules/rimraf": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", - "integrity": "sha512-33Fa/MIw/3F9KcDE/uJ2OuYUyxY+fkmw1c20DFnyhP7dfo2+BexeE1thjluPiJaG8sW6CcaqnTffwpRd4NAiTg==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "MIT", - "bin": { - "rimraf": "bin.js" + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/grunt-ts/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, + "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" }, - "node_modules/grunt-ts/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", + "license": "MIT" }, - "node_modules/grunt-ts/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "license": "MIT" }, - "node_modules/grunt-ts/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/grunt/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/grunt/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/grunt/node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.7" + "node": ">= 12.0.0" }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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==", + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">= 0.4" + "node": ">=18.12.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/lint-staged" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", "dev": true, "license": "MIT", "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "cli-truncate": "^4.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": ">=0.10.0" + "node": ">=18.0.0" } }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "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==", "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "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==", + "dev": true, + "license": "MIT" + }, + "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==", "dev": true, "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "bin": { - "he": "bin/he" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "license": "BSD-3-Clause", + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { - "parse-passwd": "^1.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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", + "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==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^11.1.0" + "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": "^20.17.0 || >=22.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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" + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "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==", "dev": true, "license": "MIT" }, - "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==", + "node_modules/log-update/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==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "get-east-asian-width": "^1.3.1" }, "engines": { - "node": ">= 14" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/log-update/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==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "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==", "dev": true, "license": "MIT", - "bin": { - "husky": "bin.js" + "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/typicode" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "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", + "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==", + "dev": true, + "license": "MIT", "dependencies": { - "minimatch": "^10.0.3" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/image-q": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", - "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", - "license": "MIT", + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", "dependencies": { - "@types/node": "16.9.1" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" } }, - "node_modules/image-q/node_modules/@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, "license": "MIT" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.8.19" + "node": "20 || >=22" } }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "node_modules/make-fetch-happen": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@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", + "ssri": "^13.0.0" + }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", - "license": "MIT" - }, - "node_modules/ios-device-lib": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.9.4.tgz", - "integrity": "sha512-UoR3+JZ4Ox9xSbp4sM6kMQu7JL5RLqgSS4gLy39hAYYtZqMpNB/u47uN63ZEq/RhqtrNFub/w7t3QicLLQ9bvg==", - "license": "Apache-2.0", - "dependencies": { - "bufferpack": "0.0.6", - "uuid": "8.3.2" - } - }, - "node_modules/ios-device-lib/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/ios-mobileprovision-finder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ios-mobileprovision-finder/-/ios-mobileprovision-finder-1.2.1.tgz", - "integrity": "sha512-9g1AJFRjWdCVdNKn7FpGv/t3EK5wFqP6EEhFVdUk7Pr8n4cVkFGyKa+l/XQdfjVUN5PnLHj5MY3x31Q3X8r80g==", - "license": "Apache-2.0", - "dependencies": { - "chalk": "^5.2.0", - "plist": "^3.0.6", - "yargs": "^17.7.1" - }, - "bin": { - "ios-mobileprovision-finder": "src/ios-mobileprovision-finder.js" - } - }, - "node_modules/ios-sim-portable": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/ios-sim-portable/-/ios-sim-portable-4.5.1.tgz", - "integrity": "sha512-g7qWaHXiFw7MuMrbwSQjeBpPl5zoHyhA6xArT2yhYg3U+WsoEy6d7x1Mgf6J77B+tvBJDWMjf2fjpuRBbQZggA==", - "license": "Apache-2.0", - "dependencies": { - "bplist-parser": "0.3.2", - "lodash": "4.17.21", - "plist": "3.0.6", - "shelljs": "~0.9.2", - "yargs": "17.7.1" - }, - "bin": { - "ios-sim-portable": "bin/ios-sim-portable.js", - "isim": "bin/ios-sim-portable.js" + "marked": "bin/marked.js" }, "engines": { - "node": ">=6.0.0" + "node": ">= 18" } }, - "node_modules/ios-sim-portable/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" }, "engines": { - "node": ">=4.8" - } - }, - "node_modules/ios-sim-portable/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "node": ">=16.0.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "marked": ">=1 <16" } }, - "node_modules/ios-sim-portable/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "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", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/ios-sim-portable/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ios-sim-portable/node_modules/isexe": { + "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/ios-sim-portable/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, - "node_modules/ios-sim-portable/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "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": ">=4" + "node": ">= 8" } }, - "node_modules/ios-sim-portable/node_modules/plist": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", - "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=6" + "node": ">=8.6" } }, - "node_modules/ios-sim-portable/node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dependencies": { - "resolve": "^1.1.6" + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">= 0.10" + "node": ">=10.0.0" } }, - "node_modules/ios-sim-portable/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/ios-sim-portable/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "shebang-regex": "^1.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/ios-sim-portable/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ios-sim-portable/node_modules/shelljs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", - "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", - "license": "BSD-3-Clause", - "dependencies": { - "execa": "^1.0.0", - "fast-glob": "^3.3.2", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, + "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==", + "dev": true, + "license": "MIT", "engines": { "node": ">=18" - } - }, - "node_modules/ios-sim-portable/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" }, - "bin": { - "which": "bin/which" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ios-sim-portable/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "license": "MIT", + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "dev": true, - "license": "MIT", + "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", "dependencies": { - "hasown": "^2.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">= 0.10" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, + "node_modules/minipass-fetch/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", + "optional": true, "dependencies": { - "hasown": "^2.0.2" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "dev": true, - "license": "MIT", + "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", "dependencies": { - "hasown": "^2.0.0" + "minipass": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", + "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", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" + "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" + }, + "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", + "dependencies": { + "minipass": "^3.0.0" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "license": "MIT", + "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", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "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" }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-fullwidth-code-point": { - "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==", - "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "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", "dependencies": { - "is-extglob": "^2.1.1" + "minipass": "^7.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } + "node_modules/ms": { + "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/is-obj": { + "node_modules/mute-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", "engines": { - "node": ">=8" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", + "node_modules/nativescript-dev-xcode": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/nativescript-dev-xcode/-/nativescript-dev-xcode-0.8.2.tgz", + "integrity": "sha512-Df18n2/t6eS08ilPF8OfcF0d+l/cMyhnmT/ZYiWn0XWn2BaGPg1DgXY5vC7B0fFmfncngJmyrInJwoGF0whmVA==", + "license": "Apache-2.0", "dependencies": { - "isobject": "^3.0.1" + "simple-plist": "1.3.1", + "uuid": "^11.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=14.0.0" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, + "node_modules/nativescript-dev-xcode/node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", "license": "MIT", "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "stream-buffers": "2.2.x" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/nativescript-dev-xcode/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 5.10.0" } }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, + "node_modules/nativescript-dev-xcode/node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", "license": "MIT", "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "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", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" }, - "node_modules/is-wsl": { + "node_modules/node-emoji": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "license": "MIT", + "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.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, "engines": { - "node": ">=20" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, + "node_modules/node-gyp/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", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", - "deprecated": "This module is no longer maintained, try this instead:\n npm i nyc\nVisit https://istanbul.js.org/integrations for other alternatives.", - "dev": true, - "license": "BSD-3-Clause", + "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", "dependencies": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" + "abbrev": "^4.0.0" }, "bin": { - "istanbul": "lib/cli.js" + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul/node_modules/brace-expansion": { - "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/nopt/node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul/node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": ">=0.10.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/istanbul/node_modules/glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "license": "ISC", "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "lru-cache": "^10.0.1" }, "engines": { - "node": "*" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/istanbul/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/normalize-package-data/node_modules/lru-cache": { + "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/istanbul/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": "*" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", + "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", "dependencies": { - "minimist": "^1.2.6" + "semver": "^7.1.1" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul/node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, + "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", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" }, - "bin": { - "which": "bin/which" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "license": "BlueOak-1.0.0", + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "license": "ISC", "dependencies": { - "@isaacs/cliui": "^9.0.0" + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/jimp": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", - "integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==", - "license": "MIT", - "dependencies": { - "@jimp/core": "1.6.0", - "@jimp/diff": "1.6.0", - "@jimp/js-bmp": "1.6.0", - "@jimp/js-gif": "1.6.0", - "@jimp/js-jpeg": "1.6.0", - "@jimp/js-png": "1.6.0", - "@jimp/js-tiff": "1.6.0", - "@jimp/plugin-blit": "1.6.0", - "@jimp/plugin-blur": "1.6.0", - "@jimp/plugin-circle": "1.6.0", - "@jimp/plugin-color": "1.6.0", - "@jimp/plugin-contain": "1.6.0", - "@jimp/plugin-cover": "1.6.0", - "@jimp/plugin-crop": "1.6.0", - "@jimp/plugin-displace": "1.6.0", - "@jimp/plugin-dither": "1.6.0", - "@jimp/plugin-fisheye": "1.6.0", - "@jimp/plugin-flip": "1.6.0", - "@jimp/plugin-hash": "1.6.0", - "@jimp/plugin-mask": "1.6.0", - "@jimp/plugin-print": "1.6.0", - "@jimp/plugin-quantize": "1.6.0", - "@jimp/plugin-resize": "1.6.0", - "@jimp/plugin-rotate": "1.6.0", - "@jimp/plugin-threshold": "1.6.0", - "@jimp/types": "1.6.0", - "@jimp/utils": "1.6.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", + "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": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/jpeg-js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", - "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", - "license": "BSD-3-Clause" - }, - "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, - "license": "MIT" - }, - "node_modules/js-yaml": { - "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", + "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", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@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" }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsmin2": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz", - "integrity": "sha512-27lUmduLlYXvSPlKP53PiV2jGHZR8J0M8xPy/ccRy2AUaQRWWvVLK4ps53n8yn/Rx870z6uTLr5tpKkWVPLrnA==", - "dev": true, - "license": "The Software shall be used for Good, not Evil. (see LICENSE)" - }, - "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", "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/json-stringify-nice": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", - "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "path-key": "^2.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=4" } }, - "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" - }, - "node_modules/just-diff": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", - "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", - "license": "MIT" - }, - "node_modules/just-diff-apply": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", - "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", - "license": "MIT" - }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=12.20.0" } }, - "node_modules/liftup": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", - "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", - "dev": true, - "license": "MIT", + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10" + "wrappy": "1" } }, - "node_modules/liftup/node_modules/findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, "license": "MIT", "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" + "mimic-fn": "^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/liftup/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", - "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", - "dev": true, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.7.0" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=18.12.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/lint-staged" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/listr2": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", - "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", - "dev": true, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, - "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==", - "dev": true, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=8" } }, - "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==", - "dev": true, - "license": "MIT" + "node_modules/ora/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "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==", - "dev": true, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, - "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==", - "dev": true, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "has-flag": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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", "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", + "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": "MIT" + "license": "BlueOak-1.0.0" }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", + "node_modules/pacote": { + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", + "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", + "license": "ISC", "dependencies": { - "p-locate": "^5.0.0" + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.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": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" }, - "engines": { - "node": ">=10" + "bin": { + "pacote": "bin/index.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/parse-bmfont-xml/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4.0.0" } }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/parse-bmfont-xml/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4.0" } }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", + "node_modules/parse-conflict-json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz", + "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "json-parse-even-better-errors": "^5.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, - "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==", + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", "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" + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" }, "engines": { "node": ">=18" @@ -7361,2890 +6366,462 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "dev": true, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "dependencies": { + "parse5": "^6.0.1" } }, - "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==", - "dev": true, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "license": "MIT" }, - "node_modules/log-update/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==", - "dev": true, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/log-update/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==", - "dev": true, - "license": "MIT", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=18" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "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==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "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.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "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==", - "dev": true, - "license": "MIT", - "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": ">= 14.16" } }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } + "node_modules/pbxproj-dom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pbxproj-dom/-/pbxproj-dom-1.2.0.tgz", + "integrity": "sha512-K2czrWqA68AR0q1UXz5EBi/zoxcljrkO4RSJX0jPnVn3iyE0HYnYOzaEEDYMpueczkT/Vtdm3SCc3NM+12kMaQ==", + "license": "Apache-2.0" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, - "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "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==", - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", - "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", - "license": "ISC", - "dependencies": { - "@gar/promise-retry": "^1.0.0", - "@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", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } + "license": "ISC" }, - "node_modules/marked-terminal": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", - "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "ansi-regex": "^6.1.0", - "chalk": "^5.4.1", - "cli-highlight": "^2.1.11", - "cli-table3": "^0.6.5", - "node-emoji": "^2.2.0", - "supports-hyperlinks": "^3.1.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">=8.6" }, - "peerDependencies": { - "marked": ">=1 <16" - } - }, - "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", - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "bin": { + "pidtree": "bin/pidtree.js" }, - "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==", - "license": "MIT" - }, - "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": ">=0.10" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "license": "ISC", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "pngjs": "^6.0.0" }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "pixelmatch": "bin/pixelmatch" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=12.13.0" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "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", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", - "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^2.0.0", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "iconv-lite": "^0.7.2" - } - }, - "node_modules/minipass-fetch/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "optional": 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/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", - "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", - "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" - }, - "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", - "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", - "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" - }, - "node_modules/minipass-sized": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", - "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", - "license": "ISC", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { + "node_modules/plist": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/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/mocha/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/mocha/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/mocha/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/brace-expansion": { - "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/mocha/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/mocha/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/mocha/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/mocha/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "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" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/jackspeak": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/mocha/node_modules/js-yaml": { - "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" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/mocha/node_modules/lru-cache": { - "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/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/path-scurry": { - "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": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/mocha/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/mocha/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/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/ms": { - "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", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nativescript-dev-xcode": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/nativescript-dev-xcode/-/nativescript-dev-xcode-0.8.1.tgz", - "integrity": "sha512-AIHoah4ZEo8CUC6xb7CX0dWTKHcsoO4DL+nYVwIESVd2XQspE1pzSRMmar9/maz4rxbPeFFTEN7eknqn69+aVg==", - "license": "Apache-2.0", - "dependencies": { - "simple-plist": "1.3.1", - "uuid": "9.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/nativescript-dev-xcode/node_modules/bplist-creator": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", - "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", - "license": "MIT", - "dependencies": { - "stream-buffers": "2.2.x" - } - }, - "node_modules/nativescript-dev-xcode/node_modules/bplist-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", - "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", - "license": "MIT", - "dependencies": { - "big-integer": "1.6.x" - }, - "engines": { - "node": ">= 5.10.0" - } - }, - "node_modules/nativescript-dev-xcode/node_modules/simple-plist": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", - "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", - "license": "MIT", - "dependencies": { - "bplist-creator": "0.1.0", - "bplist-parser": "0.3.1", - "plist": "^3.0.5" - } - }, - "node_modules/nativescript-dev-xcode/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/ncp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", - "integrity": "sha512-l+pJxuLlzwp11Dy72MJgCPNwIbXdv6imaACLiEMb2TIDyr54qz+nAZeD5qDlJefveaJ+R9Ug6KuozCxRpQXO0Q==", - "dev": true, - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, - "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", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "license": "MIT" - }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", - "license": "MIT", - "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.4", - "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/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", - "engines": { - "node": ">=6" - } - }, - "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", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt/node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "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/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-bundled": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", - "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.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", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "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", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-package-arg": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", - "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", - "license": "ISC", - "dependencies": { - "hosted-git-info": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-packlist": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", - "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", - "license": "ISC", - "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^6.0.0" - }, - "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", - "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-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", - "dependencies": { - "@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/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/omggif": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", - "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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", - "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", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "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==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pacote": { - "version": "21.0.4", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", - "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.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": "^6.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", - "ssri": "^13.0.0", - "tar": "^7.4.3" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parse-bmfont-ascii": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", - "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", - "license": "MIT" - }, - "node_modules/parse-bmfont-binary": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", - "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", - "license": "MIT" - }, - "node_modules/parse-bmfont-xml": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", - "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", - "license": "MIT", - "dependencies": { - "xml-parse-from-string": "^1.0.0", - "xml2js": "^0.5.0" - } - }, - "node_modules/parse-conflict-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz", - "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==", - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^5.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT" - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/pbxproj-dom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pbxproj-dom/-/pbxproj-dom-1.2.0.tgz", - "integrity": "sha512-K2czrWqA68AR0q1UXz5EBi/zoxcljrkO4RSJX0jPnVn3iyE0HYnYOzaEEDYMpueczkT/Vtdm3SCc3NM+12kMaQ==", - "license": "Apache-2.0" - }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "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==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pixelmatch": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", - "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", - "license": "ISC", - "dependencies": { - "pngjs": "^6.0.0" - }, - "bin": { - "pixelmatch": "bin/pixelmatch" - } - }, - "node_modules/pixelmatch/node_modules/pngjs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", - "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", - "license": "MIT", - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/plist-merge-patch": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/plist-merge-patch/-/plist-merge-patch-0.2.0.tgz", - "integrity": "sha512-JAkxPVP8F+6e28Ppd2T6QpP79/pZWj1BM1mWxE+9TRMRLeAlodmK+I8Db/cp98GPxRWPPwHxUOIKR2vnCX1J4Q==", - "license": "Apache-2.0", - "dependencies": { - "lodash": "4.17.21", - "plist": "3.0.6" - } - }, - "node_modules/plist-merge-patch/node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/plist-merge-patch/node_modules/plist": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", - "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz", - "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "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", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.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, - "license": "MIT" - }, - "node_modules/proggy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", - "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/promise-all-reject-late": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", - "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/promise-call-limit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", - "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/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", - "engines": { - "node": ">= 4" - } - }, - "node_modules/promise-stream-reader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", - "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", - "license": "MIT", - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prompts/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/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", - "engines": { - "node": ">= 4" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/proxy-lib": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/proxy-lib/-/proxy-lib-0.4.1.tgz", - "integrity": "sha512-PvdxnMi+iTIjv5CWDAv5JqWcXthPwVGJ5fp1uWwsCKLsSpjfBlLTFtKIBC5kQyH6EpzxqJuUoapYP25tOpxLWQ==", - "license": "Apache-2.0" - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qr-image": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz", - "integrity": "sha512-rXKDS5Sx3YipVsqmlMJsJsk6jXylEpiHRC2+nJy66fxA5ExYyGa4PqwteW69SaVmAb2OQ18HbYriT7cGQMbduw==", - "license": "MIT" - }, - "node_modules/qrcode-terminal": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "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/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", - "deprecated": "No longer maintained. Please upgrade to a stable version.", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "1", - "string_decoder": "0.10" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/raw-body/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/read-cmd-shim": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", - "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.4.0" } }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=14.19.0" } }, - "node_modules/readable-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", - "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8.10.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">=4" } }, - "node_modules/rechoir/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, "bin": { - "resolve": "bin/resolve" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, + "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", "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, + "node_modules/proggy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", + "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/regex-not/node_modules/is-extendable": { + "node_modules/promise-all-reject-late": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "license": "MIT" - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true, - "license": "ISC" + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "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", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, + "node_modules/promise-retry/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", "engines": { - "node": ">=0.10" + "node": ">= 4" } }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", - "dev": true, + "node_modules/promise-stream-reader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", + "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", "license": "MIT", - "dependencies": { - "is-finite": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">8.0.0" } }, - "node_modules/replace": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.2.tgz", - "integrity": "sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "license": "MIT", "dependencies": { - "chalk": "2.4.2", - "minimatch": "3.0.5", - "yargs": "^15.3.1" - }, - "bin": { - "replace": "bin/replace.js", - "search": "bin/search.js" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" }, "engines": { "node": ">= 6" } }, - "node_modules/replace/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==", + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/replace/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==", - "license": "MIT" - }, - "node_modules/replace/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" } }, - "node_modules/replace/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/proper-lockfile/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", "engines": { - "node": ">=6" + "node": ">= 4" } }, - "node_modules/replace/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==", + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/replace/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } + "node_modules/proxy-lib": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/proxy-lib/-/proxy-lib-0.4.1.tgz", + "integrity": "sha512-PvdxnMi+iTIjv5CWDAv5JqWcXthPwVGJ5fp1uWwsCKLsSpjfBlLTFtKIBC5kQyH6EpzxqJuUoapYP25tOpxLWQ==", + "license": "Apache-2.0" }, - "node_modules/replace/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==", + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/replace/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==", + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT" }, - "node_modules/replace/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/replace/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==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/replace/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/qr-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz", + "integrity": "sha512-rXKDS5Sx3YipVsqmlMJsJsk6jXylEpiHRC2+nJy66fxA5ExYyGa4PqwteW69SaVmAb2OQ18HbYriT7cGQMbduw==", + "license": "MIT" }, - "node_modules/replace/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==", - "license": "MIT", - "engines": { - "node": ">=4" + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" } }, - "node_modules/replace/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } + "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/replace/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/read-cmd-shim": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", + "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/replace/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/replace/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/replace/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==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/replace/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", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/replace/node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/replace/node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/replace/node_modules/wrap-ansi/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==", - "license": "MIT" - }, - "node_modules/replace/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/replace/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/replace/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=6" + "node": ">=8.10.0" } }, "node_modules/require-directory": { @@ -10256,40 +6833,12 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/resolve": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", "license": "MIT" }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true, - "license": "MIT" - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -10336,16 +6885,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -10371,71 +6910,38 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "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/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "node": "^20.19.0 || >=22.12.0" }, - "engines": { - "node": "*" + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/run-parallel": { @@ -10481,28 +6987,12 @@ ], "license": "MIT" }, - "node_modules/safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==", - "dev": true - }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/sax": { "version": "1.4.4", @@ -10525,38 +7015,6 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10665,97 +7123,28 @@ "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shelljs/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "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" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "dev": true, + "node_modules/shelljs/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "license": "MIT", - "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": ">=6" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -10763,30 +7152,32 @@ "license": "ISC" }, "node_modules/sigstore": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", - "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/simple-git": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", - "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", "debug": "^4.4.0" }, "funding": { @@ -10815,66 +7206,31 @@ } }, "node_modules/simple-xml-to-json": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz", - "integrity": "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz", + "integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==", "license": "MIT", "engines": { "node": ">=20.12.2" } }, "node_modules/sinon": { - "version": "19.0.5", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", - "integrity": "sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.1.0.tgz", + "integrity": "sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.5", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/sinon/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sinon/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10893,23 +7249,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -10920,121 +7259,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -11064,27 +7288,22 @@ } }, "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==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 12" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/source-map-support": { @@ -11108,14 +7327,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true, - "license": "MIT" - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -11160,53 +7371,6 @@ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "license": "BSD-3-Clause" - }, "node_modules/ssri": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", @@ -11219,19 +7383,19 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" }, "node_modules/stream-buffers": { "version": "2.2.0", @@ -11307,12 +7471,6 @@ "node": ">=0.6.19" } }, - "node_modules/string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==", - "dev": true - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11327,22 +7485,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "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, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -11355,30 +7497,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -11388,19 +7506,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -11423,47 +7528,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", - "dev": true, + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "license": "MIT", "dependencies": { - "has-flag": "^1.0.0" + "@tokenizer/token": "^0.3.0" }, "engines": { - "node": ">=0.8.0" + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, "node_modules/supports-hyperlinks": { @@ -11503,23 +7581,10 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "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, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -11579,30 +7644,12 @@ "node": ">=0.8" } }, - "node_modules/tiny-lr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", - "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" - } - }, - "node_modules/tiny-lr/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } + "license": "MIT" }, "node_modules/tinycolor2": { "version": "1.6.0", @@ -11610,14 +7657,24 @@ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -11644,9 +7701,9 @@ } }, "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==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -11655,55 +7712,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=14.0.0" } }, "node_modules/to-regex-range": { @@ -11718,87 +7734,24 @@ "node": ">=8.0" } }, - "node_modules/to-regex/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", "license": "MIT", "dependencies": { + "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/treeverse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", @@ -11809,72 +7762,22 @@ } }, "node_modules/ts-morph": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz", - "integrity": "sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", + "integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==", "license": "MIT", "dependencies": { - "@ts-morph/common": "~0.26.0", + "@ts-morph/common": "~0.29.0", "code-block-writer": "^13.0.3" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "dev": true, + "license": "0BSD", + "optional": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -11899,19 +7802,6 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -11962,41 +7852,23 @@ "node": ">=0.8.0" } }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" + "node": ">=18" }, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/underscore.string/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unicode-emoji-modifier-base": { @@ -12005,181 +7877,44 @@ "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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", - "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", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/universal-analytics": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", - "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.1", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=12.18.2" - } - }, - "node_modules/universal-analytics/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4", - "yarn": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true, - "license": "MIT" + "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", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "license": "MIT", + "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", + "dependencies": { + "imurmurhash": "^0.1.4" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/utif2": { @@ -12198,9 +7933,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -12210,25 +7945,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "license": "MIT" - }, - "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "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", @@ -12260,6 +7976,200 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -12278,31 +8188,6 @@ "defaults": "^1.0.3" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/which": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", @@ -12318,11 +8203,22 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } }, "node_modules/winreg": { "version": "1.2.5", @@ -12330,16 +8226,6 @@ "integrity": "sha512-uf7tHf+tw0B1y+x+mKTLHkykBgK2KMs3g+KlzmyMbLvICSHQyB/xOFjTT8qZ3oeTFyU7Bbj4FzXitGG6jvKhYw==", "license": "BSD-2-Clause" }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -12347,13 +8233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/workerpool": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", - "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12371,25 +8250,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "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", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -12421,9 +8281,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12441,40 +8301,6 @@ } } }, - "node_modules/xcode": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", - "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", - "license": "Apache-2.0", - "dependencies": { - "simple-plist": "^1.1.0", - "uuid": "^7.0.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/xcode/node_modules/uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", @@ -12485,6 +8311,7 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, "license": "MIT", "dependencies": { "sax": ">=0.6.0", @@ -12498,6 +8325,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4.0" @@ -12512,15 +8340,6 @@ "node": ">=8.0" } }, - "node_modules/xpath": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", - "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12540,9 +8359,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -12582,44 +8401,18 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", "pend": "~1.2.0" }, "engines": { "node": ">=12" } }, - "node_modules/yauzl/node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/yazl": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz", @@ -12629,28 +8422,6 @@ "buffer-crc32": "^1.0.0" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 4ccda12dff..fd04fe9c73 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", - "main": "./lib/nativescript-cli-lib.js", - "version": "9.0.6", + "main": "./dist/lib/nativescript-cli-lib.js", + "version": "9.1.0", "author": "NativeScript ", "description": "Command-line interface for building NativeScript projects", "bin": { @@ -11,38 +11,26 @@ "ns": "./bin/tns" }, "files": [ - "bin/*", - "config", - "docs", - "!docs/html", - "lib", - "!lib/**/*.ts", - "lib/**/*.d.ts", - "!lib/**/*.js.map", - "!lib/common/test", - "!lib/common/docs/fonts", - "resources", - "setup", - "vendor", - "postinstall.js", - "preuninstall.js" + "dist" ], "scripts": { "clean": "npx rimraf node_modules package-lock.json && npm run setup", - "build": "grunt", - "build.all": "grunt test", + "clean.build": "node scripts/clean.js", + "build": "node scripts/clean.js --dist-only && npm run tsc && node scripts/generate-test-deps.js && node scripts/copy-assets.js", + "build.all": "npm test", "dev": "tsc --watch", "setup": "npm i --ignore-scripts && npx husky", - "test": "npm run tsc && mocha --config=test/.mocharc.yml", + "test": "npm run build && vitest run", "postinstall": "node postinstall.js", "preuninstall": "node preuninstall.js", - "prepack": "grunt prepare", - "postpack": "grunt set_dev_ga_id", - "mocha": "mocha", + "prepack": "node scripts/guard-root-pack.js", + "docs-jekyll": "node scripts/build-docs.js", "tsc": "tsc", - "test-watch": "node ./dev/tsc-to-mocha-watch.js", + "test-watch": "node ./dev/tsc-to-vitest-watch.js", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", - "prettier": "prettier --write ./lib/**/*{.ts,.d.ts} ./test/**/*{.ts,.d.ts}" + "prettier": "prettier --write ./lib/**/*{.ts,.d.ts} ./test/**/*{.ts,.d.ts}", + "build.release": "npm run clean.build && tsc -p tsconfig.release.json && node scripts/generate-test-deps.js && node scripts/copy-assets.js --release && node scripts/set-ga-id.js live --dir dist && node scripts/set-ga-id.js verify --dir dist", + "pack.release": "npm run build.release && npm pack ./dist" }, "repository": { "type": "git", @@ -55,12 +43,11 @@ ], "dependencies": { "@foxt/js-srp": "0.0.3-patch2", - "@nativescript/doctor": "2.0.17", + "@nativescript/doctor": "2.0.18", "@nativescript/hook": "3.0.5", "@npmcli/arborist": "9.1.8", - "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", - "axios": "1.13.5", + "axios": "1.18.1", "byline": "5.0.0", "chokidar": "^3.6.0", "cli-table3": "0.6.5", @@ -68,27 +55,25 @@ "convert-source-map": "2.0.0", "detect-newline": "3.1.0", "email-validator": "2.0.4", - "esprima": "4.0.1", "font-finder": "1.1.0", - "ios-device-lib": "0.9.4", + "ios-device-lib": "0.9.5", "ios-mobileprovision-finder": "1.2.1", "ios-sim-portable": "4.5.1", - "jimp": "1.6.0", - "lodash": "4.17.23", + "jimp": "1.6.1", + "lodash": "4.18.1", "log4js": "6.9.1", "marked": "15.0.12", "marked-terminal": "7.3.0", - "minimatch": "10.2.4", + "minimatch": "10.2.6", "mkdirp": "3.0.1", "mute-stream": "2.0.0", - "nativescript-dev-xcode": "0.8.1", + "nativescript-dev-xcode": "0.8.2", "open": "8.4.2", "ora": "5.4.1", "pacote": "21.0.4", "pbxproj-dom": "1.2.0", "plist": "3.1.0", - "plist-merge-patch": "0.2.0", - "prettier": "3.7.3", + "prettier": "3.9.6", "prompts": "2.4.2", "proper-lockfile": "4.1.2", "proxy-lib": "0.4.1", @@ -96,17 +81,16 @@ "qrcode-terminal": "0.12.0", "semver": "7.7.3", "shelljs": "0.10.0", - "simple-git": "3.30.0", + "simple-git": "3.36.0", "simple-plist": "1.4.0", - "source-map": "0.7.6", - "tar": "7.5.9", - "ts-morph": "25.0.1", + "source-map": "0.8.0", + "tar": "7.5.22", + "ts-morph": "28.0.0", "tunnel": "0.0.6", "typescript": "5.7.3", - "universal-analytics": "0.5.3", - "uuid": "11.1.0", + "uuid": "11.1.1", "winreg": "1.2.5", - "ws": "8.18.3", + "ws": "8.21.1", "xml2js": "0.6.2", "yargs": "17.7.2", "yazl": "^3.3.1" @@ -129,11 +113,10 @@ "@types/retry": "0.12.5", "@types/semver": "7.7.1", "@types/shelljs": "^0.8.11", - "@types/sinon": "^17.0.3", + "@types/sinon": "^22.0.0", "@types/tabtab": "^3.0.2", - "@types/tar": "6.1.13", + "@types/tar": "7.0.87", "@types/tunnel": "0.0.7", - "@types/universal-analytics": "0.4.8", "@types/uuid": "^10.0.0", "@types/ws": "8.18.1", "@types/xml2js": "0.4.14", @@ -143,43 +126,18 @@ "chai": "5.3.3", "chai-as-promised": "8.0.2", "conventional-changelog-cli": "^5.0.0", + "dotenv": "17.4.2", "fast-check": "3.23.2", - "grunt": "1.6.1", - "grunt-contrib-clean": "2.0.1", - "grunt-contrib-copy": "1.0.0", - "grunt-contrib-watch": "1.1.0", - "grunt-shell": "4.0.0", - "grunt-template": "1.0.0", - "grunt-ts": "6.0.0-beta.22", "husky": "9.1.7", - "istanbul": "0.4.5", "lint-staged": "~15.5.2", - "mocha": "11.7.5", - "sinon": "19.0.5", + "sinon": "22.1.0", "source-map-support": "0.5.21", + "vitest": "^4.1.10", "xml2js": ">=0.5.0" }, "optionalDependencies": { "fsevents": "*" }, - "overrides": { - "@conventional-changelog/git-client": "2.5.1", - "jimp": { - "xml2js": "0.6.2" - }, - "npm-watch": { - "nodemon": "3.0.3" - }, - "grunt": { - "minimatch": "3.1.5" - }, - "globule": { - "minimatch": "3.1.5" - }, - "replace": { - "minimatch": "3.1.5" - } - }, "analyze": true, "license": "Apache-2.0", "engines": { diff --git a/packages/doctor/.gitignore b/packages/doctor/.gitignore index 46df90d80d..ce12338b90 100644 --- a/packages/doctor/.gitignore +++ b/packages/doctor/.gitignore @@ -17,9 +17,6 @@ coverage # nyc test coverage .nyc_output -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - # node-waf configuration .lock-wscript @@ -29,7 +26,6 @@ build/Release # Dependency directories node_modules jspm_packages -package-lock.json # Optional npm cache directory .npm @@ -64,6 +60,8 @@ test-reports.xml *.js *.js.map -/src/.d.ts -.d.ts !/*.js +!/scripts/*.js + +/dist/ +/dist-test/ diff --git a/packages/doctor/.npmrc b/packages/doctor/.npmrc deleted file mode 100644 index 521a9f7c07..0000000000 --- a/packages/doctor/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps=true diff --git a/packages/doctor/Gruntfile.js b/packages/doctor/Gruntfile.js deleted file mode 100644 index b24e5bc02c..0000000000 --- a/packages/doctor/Gruntfile.js +++ /dev/null @@ -1,91 +0,0 @@ -module.exports = function (grunt) { - grunt.initConfig({ - ts: { - options: grunt.file.readJSON("tsconfig.json").compilerOptions, - - devsrc: { - src: ["src/**/*.ts", "typings/**/*.ts"], - reference: "src/.d.ts", - }, - - devall: { - src: ["src/**/*.ts", "test/**/*.ts", "typings/**/*.ts"], - reference: "src/.d.ts", - }, - - release_build: { - src: ["src/**/*.ts", "test/**/*.ts", "typings/**/*.ts"], - reference: "src/.d.ts", - options: { - sourceMap: false, - removeComments: true, - }, - }, - }, - - tslint: { - build: { - files: { - src: ["src/**/*.ts", "test/**/*.ts", "typings/**/*.ts", "!**/*.d.ts"], - }, - options: { - configuration: grunt.file.readJSON("./tslint.json"), - project: "tsconfig.json", - }, - }, - }, - - watch: { - devall: { - files: ["src/**/*.ts", "test/**/*.ts"], - tasks: ["ts:devall", "shell:npm_test"], - options: { - atBegin: true, - interrupt: true, - }, - }, - ts: { - files: ["src/**/*.ts", "test/**/*.ts"], - tasks: ["ts:devall"], - options: { - atBegin: true, - interrupt: true, - }, - }, - }, - - shell: { - options: { - stdout: true, - stderr: true, - failOnError: true, - }, - npm_test: { - command: "npm test", - }, - }, - - clean: { - src: [ - "test/**/*.js*", - "src/**/*.js*", - "!src/hooks/**/*.js", - "!Gruntfile.js", - "*.tgz", - ], - }, - }); - - grunt.loadNpmTasks("grunt-contrib-clean"); - grunt.loadNpmTasks("grunt-contrib-watch"); - grunt.loadNpmTasks("grunt-shell"); - grunt.loadNpmTasks("grunt-ts"); - grunt.loadNpmTasks("grunt-tslint"); - - grunt.registerTask("test", ["ts:devall", "shell:npm_test"]); - grunt.registerTask("pack", ["clean", "ts:release_build", "shell:npm_test"]); - grunt.registerTask("lint", ["tslint:build"]); - grunt.registerTask("all", ["clean", "test", "lint"]); - grunt.registerTask("rebuild", ["clean", "ts:devsrc"]); - grunt.registerTask("default", "ts:devsrc"); -}; diff --git a/packages/doctor/package-lock.json b/packages/doctor/package-lock.json new file mode 100644 index 0000000000..85b5ed0910 --- /dev/null +++ b/packages/doctor/package-lock.json @@ -0,0 +1,2513 @@ +{ + "name": "@nativescript/doctor", + "version": "2.0.18", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nativescript/doctor", + "version": "2.0.18", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1", + "semver": "7.7.3", + "shelljs": "0.10.0", + "winreg": "1.2.5", + "yauzl": "^3.4.0" + }, + "devDependencies": { + "@types/chai": "5.2.2", + "@types/lodash": "4.17.21", + "@types/semver": "7.7.1", + "@types/shelljs": "0.8.17", + "@types/temp": "0.9.4", + "@types/winreg": "1.2.36", + "@types/yauzl": "2.10.3", + "chai": "5.3.3", + "conventional-changelog-cli": "^5.0.0", + "rimraf": "6.1.2", + "typescript": "~5.9.2", + "vitest": "^4.1.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@conventional-changelog/git-client": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/child-process-utils": "^1.0.0", + "@simple-libs/stream-utils": "^1.2.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.4.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hutson/parse-repository-url": { + "version": "5.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", + "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^2.0.0-alpha.3", + "@emnapi/runtime": "^2.0.0-alpha.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "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", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-libs/child-process-utils": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/shelljs": { + "version": "0.8.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "glob": "^11.0.3" + } + }, + "node_modules/@types/temp": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/winreg": { + "version": "1.2.36", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/add-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/conventional-changelog": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-atom": "^5.0.0", + "conventional-changelog-codemirror": "^5.0.0", + "conventional-changelog-conventionalcommits": "^8.0.0", + "conventional-changelog-core": "^8.0.0", + "conventional-changelog-ember": "^5.0.0", + "conventional-changelog-eslint": "^6.0.0", + "conventional-changelog-express": "^5.0.0", + "conventional-changelog-jquery": "^6.0.0", + "conventional-changelog-jshint": "^5.0.0", + "conventional-changelog-preset-loader": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "8.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-atom": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-cli": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "add-stream": "^1.0.0", + "conventional-changelog": "^6.0.0", + "meow": "^13.0.0", + "tempfile": "^5.0.0" + }, + "bin": { + "conventional-changelog": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-codemirror": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "8.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-core": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@hutson/parse-repository-url": "^5.0.0", + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-parser": "^6.0.0", + "git-raw-commits": "^5.0.0", + "git-semver-tags": "^8.0.0", + "hosted-git-info": "^7.0.0", + "normalize-package-data": "^6.0.0", + "read-package-up": "^11.0.0", + "read-pkg": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-ember": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-eslint": { + "version": "6.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-express": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-jquery": { + "version": "6.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-jshint": { + "version": "5.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "8.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "conventional-commits-filter": "^5.0.0", + "handlebars": "^4.7.7", + "meow": "^13.0.0", + "semver": "^7.5.2" + }, + "bin": { + "conventional-changelog-writer": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-filter": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-parser": { + "version": "6.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "license": "ISC" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "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/fastq": { + "version": "1.20.1", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/git-client": "^2.6.0", + "meow": "^13.0.0" + }, + "bin": { + "git-raw-commits": "src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/git-semver-tags": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/git-client": "^2.6.0", + "meow": "^13.0.0" + }, + "bin": { + "git-semver-tags": "src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "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/read-package-up": { + "version": "11.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "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/semver": { + "version": "7.7.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.10.0", + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^5.1.1", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempfile": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "temp-dir": "^3.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winreg": { + "version": "1.2.5", + "license": "BSD-2-Clause" + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/packages/doctor/package.json b/packages/doctor/package.json index 50bff723d6..1ee006bc89 100644 --- a/packages/doctor/package.json +++ b/packages/doctor/package.json @@ -1,27 +1,31 @@ { "name": "@nativescript/doctor", - "version": "2.0.17", + "version": "2.0.18", "description": "Library that helps identifying if the environment can be used for development of {N} apps.", - "main": "src/index.js", + "main": "dist/index.js", "types": "./typings/nativescript-doctor.d.ts", "files": [ - "src/**/*.js", + "dist/**/*.js", + "dist/**/*.d.ts", "resources", "typings", "CHANGELOG.md", "NOTICE.txt" ], "scripts": { - "clean": "npx rimraf node_modules package-lock.json && npm i && grunt clean", - "build": "grunt", - "build.all": "grunt ts:devall", - "prepack": "grunt pack", - "test": "istanbul cover ./node_modules/mocha/bin/_mocha -- --recursive", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" + "clean": "npx rimraf node_modules package-lock.json && npm i && npm run clean.build", + "clean.build": "node scripts/clean.js", + "build": "tsc", + "dev": "tsc --watch", + "prepack": "npm run clean.build && npm test && tsc -p tsconfig.release.json", + "test": "tsc -p tsconfig.test.json && vitest run", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", + "test-watch": "vitest" }, "repository": { "type": "git", - "url": "git+https://github.com/NativeScript/nativescript-doctor.git" + "url": "git+https://github.com/NativeScript/nativescript-cli.git", + "directory": "packages/doctor" }, "keywords": [ "NativeScript", @@ -37,13 +41,12 @@ ], "license": "Apache-2.0", "bugs": { - "url": "https://github.com/NativeScript/nativescript-doctor/issues" + "url": "https://github.com/NativeScript/nativescript-cli/issues" }, - "homepage": "https://github.com/NativeScript/nativescript-doctor#readme", + "homepage": "https://github.com/NativeScript/nativescript-cli/tree/main/packages/doctor#readme", "devDependencies": { "@types/chai": "5.2.2", "@types/lodash": "4.17.21", - "@types/mocha": "10.0.10", "@types/semver": "7.7.1", "@types/shelljs": "0.8.17", "@types/temp": "0.9.4", @@ -51,24 +54,15 @@ "@types/yauzl": "2.10.3", "chai": "5.3.3", "conventional-changelog-cli": "^5.0.0", - "grunt": "1.6.1", - "grunt-contrib-clean": "2.0.1", - "grunt-contrib-watch": "1.1.0", - "grunt-shell": "4.0.0", - "grunt-ts": "6.0.0-beta.22", - "grunt-tslint": "5.0.2", - "istanbul": "0.4.5", - "mocha": "11.7.5", "rimraf": "6.1.2", - "tslint": "6.1.3", - "tslint-microsoft-contrib": "6.2.0", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "vitest": "^4.1.10" }, "dependencies": { - "lodash": "4.17.21", + "lodash": "^4.18.1", "semver": "7.7.3", "shelljs": "0.10.0", "winreg": "1.2.5", - "yauzl": "3.2.0" + "yauzl": "^3.4.0" } -} \ No newline at end of file +} diff --git a/packages/doctor/scripts/clean.js b/packages/doctor/scripts/clean.js new file mode 100644 index 0000000000..7c210e738d --- /dev/null +++ b/packages/doctor/scripts/clean.js @@ -0,0 +1,30 @@ +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const rootDir = path.join(__dirname, ".."); + +// Older checkouts compiled next to the sources; .gitignore is the source of +// truth for which files under src/ and test/ are leftover compiler output. +const result = child_process.spawnSync("git", ["clean", "-Xdf", "src", "test"], { + cwd: rootDir, + stdio: "inherit", +}); + +if (result.error) { + throw result.error; +} + +if (result.status !== 0) { + throw new Error(`git clean exited with status ${result.status}`); +} + +for (const dir of ["dist", "coverage"]) { + fs.rmSync(path.join(rootDir, dir), { recursive: true, force: true }); +} + +for (const entry of fs.readdirSync(rootDir)) { + if (entry.endsWith(".tgz")) { + fs.rmSync(path.join(rootDir, entry)); + } +} diff --git a/packages/doctor/src/android-tools-info.ts b/packages/doctor/src/android-tools-info.ts index 90b7ad5989..9c118e676c 100644 --- a/packages/doctor/src/android-tools-info.ts +++ b/packages/doctor/src/android-tools-info.ts @@ -33,6 +33,8 @@ export class AndroidToolsInfo implements NativeScriptDoctor.IAndroidToolsInfo { "android-34", "android-35", "android-36", + "android-36.1", + "android-37", ]; const isRuntimeVersionLessThan = (targetVersion: string) => { @@ -494,12 +496,29 @@ export class AndroidToolsInfo implements NativeScriptDoctor.IAndroidToolsInfo { installedTargets: string[], projectDir: string, ): string { + // SDK platforms newer than android-36 may install into directories named + // "android-." (e.g. "android-37.0") with no plain + // "android-" directory, so installed targets are matched on their + // API level rather than the exact directory name. Extension directories + // like "android-33-ext4" are deliberately not treated as the base + // platform - they don't contain a full SDK. return _.findLast( this.getSupportedTargets(projectDir).sort(), - (supportedTarget) => _.includes(installedTargets, supportedTarget), + (supportedTarget) => + _.includes(installedTargets, supportedTarget) || + installedTargets.some( + (installedTarget) => + AndroidToolsInfo.getInstalledTargetApiLevel(installedTarget) === + this.parseAndroidSdkString(supportedTarget), + ), ); } + private static getInstalledTargetApiLevel(installedTarget: string): number { + const match = installedTarget.match(/^android-(\d+)(?:\.\d+)?$/); + return match ? parseInt(match[1], 10) : null; + } + private parseAndroidSdkString(androidSdkString: string): number { return parseInt( androidSdkString.replace(`${this.ANDROID_TARGET_PREFIX}-`, ""), @@ -657,20 +676,4 @@ export class AndroidToolsInfo implements NativeScriptDoctor.IAndroidToolsInfo { this._cachedRuntimeVersion = runtimeVersion; return runtimeVersion; } - - private getMaxSupportedCompileVersion( - config: Partial & { - runtimeVersion?: string; - }, - ): number { - if ( - config.runtimeVersion && - semver.lt(semver.coerce(config.runtimeVersion), "6.1.0") - ) { - return 28; - } - return this.parseAndroidSdkString( - _.last(this.getSupportedTargets(config.projectDir).sort()), - ); - } } diff --git a/packages/doctor/src/doctor.ts b/packages/doctor/src/doctor.ts index 3fc13bae78..5557fd6777 100644 --- a/packages/doctor/src/doctor.ts +++ b/packages/doctor/src/doctor.ts @@ -3,7 +3,6 @@ import { EOL } from "os"; import { HostInfo } from "./host-info"; import { AndroidLocalBuildRequirements } from "./local-build-requirements/android-local-build-requirements"; import { IosLocalBuildRequirements } from "./local-build-requirements/ios-local-build-requirements"; -import { Helpers } from "./helpers"; import * as semver from "semver"; export class Doctor implements NativeScriptDoctor.IDoctor { @@ -11,17 +10,16 @@ export class Doctor implements NativeScriptDoctor.IDoctor { constructor( private androidLocalBuildRequirements: AndroidLocalBuildRequirements, - private helpers: Helpers, private hostInfo: HostInfo, private iOSLocalBuildRequirements: IosLocalBuildRequirements, private sysInfo: NativeScriptDoctor.ISysInfo, - private androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo + private androidToolsInfo: NativeScriptDoctor.IAndroidToolsInfo, ) {} public async canExecuteLocalBuild( platform: string, projectDir?: string, - runtimeVersion?: string + runtimeVersion?: string, ): Promise { this.validatePlatform(platform); @@ -30,7 +28,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { ) { return await this.androidLocalBuildRequirements.checkRequirements( projectDir, - runtimeVersion + runtimeVersion, ); } else if ( platform.toLowerCase() === Constants.IOS_PLATFORM_NAME.toLowerCase() @@ -42,7 +40,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { } public async getInfos( - config?: NativeScriptDoctor.ISysInfoConfig + config?: NativeScriptDoctor.ISysInfoConfig, ): Promise { let result: NativeScriptDoctor.IInfo[] = []; const sysInfoData = await this.sysInfo.getSysInfo(config); @@ -57,8 +55,8 @@ export class Doctor implements NativeScriptDoctor.IDoctor { this.getAndroidInfos( sysInfoData, config && config.projectDir, - config && config.androidRuntimeVersion - ) + config && config.androidRuntimeVersion, + ), ); } @@ -85,7 +83,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { } public async getWarnings( - config?: NativeScriptDoctor.ISysInfoConfig + config?: NativeScriptDoctor.ISysInfoConfig, ): Promise { const info = await this.getInfos(config); return info @@ -96,7 +94,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { private getAndroidInfos( sysInfoData: NativeScriptDoctor.ISysInfoData, projectDir?: string, - runtimeVersion?: string + runtimeVersion?: string, ): NativeScriptDoctor.IInfo[] { let result: NativeScriptDoctor.IInfo[] = []; @@ -144,7 +142,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { warnings: this.androidToolsInfo.validateJavacVersion( sysInfoData.javacVersion, projectDir, - runtimeVersion + runtimeVersion, ), infoMessage: "Javac is installed and is configured properly.", platforms: [Constants.ANDROID_PLATFORM_NAME], @@ -164,14 +162,14 @@ export class Doctor implements NativeScriptDoctor.IDoctor { EOL + "described in http://docs.oracle.com/javase/8/docs/technotes/guides/install/install_overview.html (for JDK 8).", platforms: [Constants.ANDROID_PLATFORM_NAME], - }) + }), ); return result; } private async getiOSInfos( - sysInfoData: NativeScriptDoctor.ISysInfoData + sysInfoData: NativeScriptDoctor.ISysInfoData, ): Promise { let result: NativeScriptDoctor.IInfo[] = []; if (this.hostInfo.isDarwin) { @@ -216,7 +214,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { warningMessage: "CocoaPods update required.", additionalInformation: `You are using CocoaPods version ${sysInfoData.cocoaPodsVer} which does not support Xcode ${sysInfoData.xcodeVer} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH.Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`, platforms: [Constants.IOS_PLATFORM_NAME], - }) + }), ); if (sysInfoData.xcodeVer && sysInfoData.cocoaPodsVer) { @@ -230,7 +228,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { additionalInformation: "Verify that CocoaPods are configured properly.", platforms: [Constants.IOS_PLATFORM_NAME], - }) + }), ); } @@ -241,7 +239,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { !semver.valid(sysInfoData.cocoaPodsVer) || !semver.lt( sysInfoData.cocoaPodsVer, - Doctor.MIN_SUPPORTED_POD_VERSION + Doctor.MIN_SUPPORTED_POD_VERSION, ), infoMessage: `Your current CocoaPods version is newer than ${Doctor.MIN_SUPPORTED_POD_VERSION}.`, warningMessage: `Your current CocoaPods version is earlier than ${Doctor.MIN_SUPPORTED_POD_VERSION}.`, @@ -260,7 +258,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { EOL + `Error while validating Python packages. Error is: ${sysInfoData.pythonInfo.installationErrorMessage}`, platforms: [Constants.IOS_PLATFORM_NAME], - }) + }), ); if (sysInfoData.xcodeVer) { @@ -272,7 +270,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { additionalInformation: "To build your application for iOS, update your Xcode.", platforms: [Constants.IOS_PLATFORM_NAME], - }) + }), ); } } @@ -322,7 +320,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { } private convertWarningToInfo( - warning: NativeScriptDoctor.IWarning + warning: NativeScriptDoctor.IWarning, ): NativeScriptDoctor.IInfo { return { message: warning.warning, @@ -333,7 +331,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { } private convertInfoToWarning( - info: NativeScriptDoctor.IInfo + info: NativeScriptDoctor.IInfo, ): NativeScriptDoctor.IWarning { return { warning: info.message, @@ -345,7 +343,7 @@ export class Doctor implements NativeScriptDoctor.IDoctor { private isPlatformSupported(platform: string): boolean { return ( Constants.SUPPORTED_PLATFORMS.map((pl) => pl.toLowerCase()).indexOf( - platform.toLowerCase() + platform.toLowerCase(), ) !== -1 ); } @@ -358,8 +356,8 @@ export class Doctor implements NativeScriptDoctor.IDoctor { if (!this.isPlatformSupported(platform)) { throw new Error( `Platform ${platform} is not supported.The supported platforms are: ${Constants.SUPPORTED_PLATFORMS.join( - ", " - )} ` + ", ", + )} `, ); } } diff --git a/packages/doctor/src/index.ts b/packages/doctor/src/index.ts index dfc57bc065..b1a9a7fb90 100644 --- a/packages/doctor/src/index.ts +++ b/packages/doctor/src/index.ts @@ -19,7 +19,7 @@ const androidToolsInfo = new AndroidToolsInfo( childProcess, fileSystem, hostInfo, - helpers + helpers, ); const sysInfo: NativeScriptDoctor.ISysInfo = new SysInfo( @@ -28,25 +28,24 @@ const sysInfo: NativeScriptDoctor.ISysInfo = new SysInfo( helpers, hostInfo, winReg, - androidToolsInfo + androidToolsInfo, ); const androidLocalBuildRequirements = new AndroidLocalBuildRequirements( androidToolsInfo, - sysInfo + sysInfo, ); const iOSLocalBuildRequirements = new IosLocalBuildRequirements( sysInfo, - hostInfo + hostInfo, ); const doctor: NativeScriptDoctor.IDoctor = new Doctor( androidLocalBuildRequirements, - helpers, hostInfo, iOSLocalBuildRequirements, sysInfo, - androidToolsInfo + androidToolsInfo, ); const setShouldCacheSysInfo = sysInfo.setShouldCacheSysInfo.bind(sysInfo); diff --git a/packages/doctor/src/sys-info.ts b/packages/doctor/src/sys-info.ts index 7c71d43491..9bc9b662c0 100644 --- a/packages/doctor/src/sys-info.ts +++ b/packages/doctor/src/sys-info.ts @@ -424,6 +424,39 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo { tempDirectory, ); const xcodeProjectDir = path.join(tempDirectory, "cocoapods"); + + // If asdf version manager is installed, get the current Ruby version for the project directory and write it to the temporary project directory. + // Resolve relative to the directory `ns doctor` was invoked from, since it can be run outside of a project directory. + const asdfResult = await this.childProcess.spawnFromEvent( + "asdf", + ["current", "ruby"], + "exit", + { ignoreError: true, spawnOptions: { cwd: process.cwd() } }, + ); + + if (asdfResult.exitCode === 0) { + const asdfVersionMatch = (asdfResult.stdout as string).match( + SysInfo.VERSION_REGEXP, + ); + + if (asdfVersionMatch?.[0]) { + const asdfVersion = asdfVersionMatch[0]; + const asdfConfigPath = path.join( + xcodeProjectDir, + ".tool-versions", + ); + const wroteASDFConfig = this.fileSystem.appendFile( + asdfConfigPath, + `ruby ${asdfVersion}\n`, + ); + if (!wroteASDFConfig) { + console.warn( + `CocoaPods invocation may fail, check asdf config`, + ); + } + } + } + const spawnResult = await this.childProcess.spawnFromEvent( "pod", ["install"], @@ -432,6 +465,7 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo { ); return !spawnResult.exitCode; } catch (err) { + console.log(`Pod command failed - ${err}`); return false; } finally { this.fileSystem.deleteEntry(tempDirectory); diff --git a/packages/doctor/src/wrappers/file-system.ts b/packages/doctor/src/wrappers/file-system.ts index ac7a05ed95..759c826d0c 100644 --- a/packages/doctor/src/wrappers/file-system.ts +++ b/packages/doctor/src/wrappers/file-system.ts @@ -8,6 +8,17 @@ export class FileSystem { return fs.existsSync(path.resolve(filePath)); } + public appendFile(filePath: string, text: string): boolean { + let success = false; + try { + fs.appendFileSync(path.resolve(filePath), text); + success = true; + } catch (err) { + console.error(`appendFile failed with ${err}`); + } + return success; + } + public extractZip(pathToZip: string, outputDir: string): Promise { return new Promise((resolve, reject) => { yauzl.open( @@ -46,7 +57,7 @@ export class FileSystem { zipFile.once("end", () => resolve()); zipFile.readEntry(); - } + }, ); }); } @@ -57,7 +68,7 @@ export class FileSystem { public readJson( filePath: string, - options?: { encoding?: null; flag?: string } + options?: { encoding?: null; flag?: string }, ): T { const content = fs.readFileSync(filePath, options); return JSON.parse(content.toString()); diff --git a/packages/doctor/test/android-tools-info.ts b/packages/doctor/test/android-tools-info.ts index f8121b0da6..d323eb5a26 100644 --- a/packages/doctor/test/android-tools-info.ts +++ b/packages/doctor/test/android-tools-info.ts @@ -23,10 +23,13 @@ describe("androidToolsInfo", () => { EOL + " described in " + Constants.SYSTEM_REQUIREMENTS_LINKS; - before(() => { + beforeAll(() => { process.env["ANDROID_HOME"] = "test"; }); - const getAndroidToolsInfo = (runtimeVersion?: string): AndroidToolsInfo => { + const getAndroidToolsInfo = ( + runtimeVersion?: string, + installedTargets?: string[], + ): AndroidToolsInfo => { const childProcess: ChildProcess = {}; const fs: FileSystem = { exists: () => true, @@ -59,19 +62,22 @@ describe("androidToolsInfo", () => { "34.0.0", ]; } else { - return [ - "android-16", - "android-27", - "android-28", - "android-29", - "android-30", - "android-31", - "android-32", - "android-33", - "android-34", - "android-35", - "android-36", - ]; + return ( + installedTargets || [ + "android-16", + "android-27", + "android-28", + "android-29", + "android-30", + "android-31", + "android-32", + "android-33", + "android-34", + "android-35", + "android-36", + "android-36.1", + ] + ); } }, }; @@ -108,41 +114,76 @@ describe("androidToolsInfo", () => { assert.equal(toolsInfo.compileSdkVersion, 36); }); + + it("resolves android-37 from minor-versioned SDK directories", () => { + const androidToolsInfo = getAndroidToolsInfo("8.2.0", [ + "android-36", + "android-37.0", + "android-37.1", + ]); + const toolsInfo = androidToolsInfo.getToolsInfo({ projectDir: "test" }); + + assert.equal(toolsInfo.compileSdkVersion, 37); + }); + + it("resolves android-36 when only android-36.1 is installed", () => { + const androidToolsInfo = getAndroidToolsInfo("8.2.0", ["android-36.1"]); + const toolsInfo = androidToolsInfo.getToolsInfo({ projectDir: "test" }); + + assert.equal(toolsInfo.compileSdkVersion, 36); + }); + + it("does not treat extension directories as the base platform", () => { + const androidToolsInfo = getAndroidToolsInfo("8.2.0", [ + "android-33", + "android-35-ext15", + ]); + const toolsInfo = androidToolsInfo.getToolsInfo({ projectDir: "test" }); + + assert.equal(toolsInfo.compileSdkVersion, 33); + }); }); describe("supportedAndroidSdks", () => { - const assertSupportedRange = ( + const assertSupportedTargets = ( runtimeVersion: string, - min: number, - max: number, + expectedTargets: string[], ) => { - let cnt = 0; const androidToolsInfo = getAndroidToolsInfo(runtimeVersion); const supportedTargets = androidToolsInfo.getSupportedTargets("test"); - for (let i = 0; i < supportedTargets.length; i++) { - assert.equal(supportedTargets[i], `android-${min + i}`); - cnt = min + i; - } - assert.equal(cnt, max); + assert.deepEqual(supportedTargets, expectedTargets); }; it("runtime 6.0.0 should support android-17 - android-28", () => { - const min = 17; - const max = 28; - assertSupportedRange("6.0.0", min, max); + assertSupportedTargets( + "6.0.0", + Array.from({ length: 12 }, (_, index) => `android-${17 + index}`), + ); }); it("runtime 8.1.0 should support android-17 - android-30", () => { - const min = 17; - const max = 30; - assertSupportedRange("8.1.0", min, max); + assertSupportedTargets( + "8.1.0", + Array.from({ length: 14 }, (_, index) => `android-${17 + index}`), + ); }); - it("runtime 8.2.0 should support android-17 - android-34", () => { - const min = 17; - const max = 36; - assertSupportedRange("8.2.0", min, max); - assertSupportedRange("8.3.0", min, max); + it("runtime 8.2.0 should support android-17 through android-37 including android-36.1", () => { + const expectedTargets = [ + ...Array.from({ length: 20 }, (_, index) => `android-${17 + index}`), + "android-36.1", + "android-37", + ]; + assertSupportedTargets("8.2.0", expectedTargets); + }); + + it("runtime 8.3.0 should support android-17 through android-37 including android-36.1", () => { + const expectedTargets = [ + ...Array.from({ length: 20 }, (_, index) => `android-${17 + index}`), + "android-36.1", + "android-37", + ]; + assertSupportedTargets("8.3.0", expectedTargets); }); }); @@ -374,6 +415,16 @@ describe("androidToolsInfo", () => { targetSdk: 32, expectWarning: false, }, + { + runtimeVersion: "8.2.0", + targetSdk: 37, + expectWarning: false, + }, + { + runtimeVersion: "8.2.0", + targetSdk: 38, + expectWarning: true, + }, ]; testCases.forEach(({ runtimeVersion, targetSdk, expectWarning }) => { @@ -398,7 +449,7 @@ describe("androidToolsInfo", () => { }); }); - after(() => { + afterAll(() => { process.env["ANDROID_HOME"] = originalAndroidHome; }); }); diff --git a/packages/doctor/test/sys-info.ts b/packages/doctor/test/sys-info.ts index 5b41c32487..2d3c64ea9b 100644 --- a/packages/doctor/test/sys-info.ts +++ b/packages/doctor/test/sys-info.ts @@ -1,4 +1,5 @@ import * as assert from "assert"; +import * as fs from "fs"; import * as path from "path"; import { EOL } from "os"; import { SysInfo } from "../src/sys-info"; @@ -228,7 +229,6 @@ describe("SysInfo unit tests", () => { }); describe("Should execute correct commands to check for", () => { - let spawnFromEventCommand: string; let execCommands: string[] = []; let fileSystem: any; let hostInfo: any; @@ -236,12 +236,7 @@ describe("SysInfo unit tests", () => { beforeEach(() => { execCommands = []; const childProcess: ChildProcess = { - spawnFromEvent: async ( - command: string, - args: string[], - event: string, - ) => { - spawnFromEventCommand = `${command} ${args.join(" ")}`; + spawnFromEvent: async () => { return { stdout: "", stderr: "" }; }, exec: async (command: string) => { @@ -910,4 +905,134 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`), }); }); }); + + describe("isCocoaPodsWorkingCorrectly", () => { + interface ICocoaPodsMockOptions { + // Mimics the ChildProcess result for `asdf current ruby`. + // When omitted, asdf is treated as not installed. + asdfResult?: { + stdout?: string; + stderr?: string; + exitCode?: number | string; + }; + podExitCode?: number; + } + + const createCocoaPodsSysInfo = (options: ICocoaPodsMockOptions) => { + const appendedFiles: { filePath: string; text: string }[] = []; + const spawnCalls: { + command: string; + options?: ISpawnFromEventOptions; + }[] = []; + + const childProcess: any = { + spawnFromEvent: async ( + command: string, + args: string[], + event: string, + spawnFromEventOptions?: ISpawnFromEventOptions, + ) => { + const fullCommand = `${command} ${args.join(" ")}`; + spawnCalls.push({ + command: fullCommand, + options: spawnFromEventOptions, + }); + + if (fullCommand === "asdf current ruby") { + // Mirror the ChildProcess wrapper: with `ignoreError` it always + // resolves, surfacing a non-zero exitCode instead of throwing when + // asdf is missing/misconfigured. + return ( + options.asdfResult || { + stdout: "", + stderr: "spawn asdf ENOENT", + exitCode: "ENOENT", + } + ); + } + + return { + stdout: "", + stderr: "", + exitCode: options.podExitCode ?? 0, + }; + }, + exec: async () => ({ stdout: "", stderr: "" }), + execFile: async (): Promise => undefined, + execSync: (): string => null, + }; + + const fileSystem: any = { + exists: () => true, + extractZip: () => Promise.resolve(), + readDirectory: (): string[] => [], + appendFile: (filePath: string, text: string) => { + appendedFiles.push({ filePath, text }); + return true; + }, + deleteEntry: (filePath: string) => + fs.rmSync(filePath, { recursive: true, force: true }), + }; + + const hostInfo: any = { + isDarwin: true, + isWindows: false, + isLinux: false, + }; + + const helpers = new Helpers(hostInfo); + const sysInfo = new SysInfo( + childProcess, + fileSystem, + helpers, + hostInfo, + null, + androidToolsInfo, + ); + + return { sysInfo, appendedFiles, spawnCalls }; + }; + + it("writes the active Ruby version to .tool-versions when asdf is available", async () => { + const { sysInfo, appendedFiles, spawnCalls } = createCocoaPodsSysInfo({ + asdfResult: { + stdout: + "ruby 3.2.1 /Users/user/app/.tool-versions", + exitCode: 0, + }, + }); + + const result = await sysInfo.isCocoaPodsWorkingCorrectly(); + + assert.deepEqual(result, true); + assert.deepEqual(appendedFiles.length, 1); + assert.ok( + appendedFiles[0].filePath.endsWith( + path.join("cocoapods", ".tool-versions"), + ), + ); + // The entry must be newline-terminated so it does not merge with existing content. + assert.deepEqual(appendedFiles[0].text, "ruby 3.2.1\n"); + + const asdfCall = spawnCalls.find( + (c) => c.command === "asdf current ruby", + ); + assert.ok(asdfCall, "expected asdf to be probed"); + // The probe must not throw when asdf is missing/misconfigured... + assert.deepEqual(asdfCall.options.ignoreError, true); + // ...and it must resolve the version relative to the invocation directory. + assert.deepEqual(asdfCall.options.spawnOptions.cwd, process.cwd()); + }); + + it("does not write .tool-versions and stays healthy when asdf is not installed", async () => { + const { sysInfo, appendedFiles } = createCocoaPodsSysInfo({ + // asdf missing -> wrapper resolves with a non-zero (ENOENT) exit code. + }); + + const result = await sysInfo.isCocoaPodsWorkingCorrectly(); + + assert.deepEqual(result, true); + assert.deepEqual(appendedFiles.length, 0); + }); + }); }); diff --git a/packages/doctor/test/vitest-globals.d.ts b/packages/doctor/test/vitest-globals.d.ts new file mode 100644 index 0000000000..9896c472fb --- /dev/null +++ b/packages/doctor/test/vitest-globals.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/doctor/test/wrappers/file-system.ts b/packages/doctor/test/wrappers/file-system.ts index f58cea8a72..f5cbd27bf3 100644 --- a/packages/doctor/test/wrappers/file-system.ts +++ b/packages/doctor/test/wrappers/file-system.ts @@ -1,6 +1,6 @@ import { tmpdir } from "os"; import { assert } from "chai"; -import { rimraf, rimrafSync } from "rimraf"; +import { rimrafSync } from "rimraf"; import { FileSystem } from "../../src/wrappers/file-system"; @@ -26,25 +26,20 @@ describe("FileSystem", () => { `${tmpDir}/test/wrappers/file-system.ts`, ]; - it("should extract in example zip archive in tmp folder", (done) => { + it("should extract in example zip archive in tmp folder", async () => { const fs = new FileSystem(); - fs.extractZip(testFilePath, tmpDir) - .then(() => { - const allExists = filesThatNeedToExist - .map(fs.exists) - .reduce((acc, r) => acc && r, true); + await fs.extractZip(testFilePath, tmpDir); - assert.isTrue(allExists); + const allExists = filesThatNeedToExist + .map(fs.exists) + .reduce((acc, r) => acc && r, true); - done(); - }) - .catch((e) => done(e)); + assert.isTrue(allExists); }); - afterEach((done) => { + afterEach(() => { rimrafSync(tmpDir); - done(); }); }); }); diff --git a/packages/doctor/tsconfig.json b/packages/doctor/tsconfig.json index 3e110d9f3a..baec4941ce 100644 --- a/packages/doctor/tsconfig.json +++ b/packages/doctor/tsconfig.json @@ -1,11 +1,13 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "skipLibCheck": true + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", + "declaration": true }, "include": [ "src/", - "test/", "typings/" ] } diff --git a/packages/doctor/tsconfig.release.json b/packages/doctor/tsconfig.release.json new file mode 100644 index 0000000000..065b45aa74 --- /dev/null +++ b/packages/doctor/tsconfig.release.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "removeComments": true + } +} diff --git a/packages/doctor/tsconfig.test.json b/packages/doctor/tsconfig.test.json new file mode 100644 index 0000000000..54daabf6f7 --- /dev/null +++ b/packages/doctor/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "declaration": false + }, + "include": ["src/", "test/", "typings/"] +} diff --git a/packages/doctor/tslint.json b/packages/doctor/tslint.json deleted file mode 100644 index 5386dd58d9..0000000000 --- a/packages/doctor/tslint.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "rulesDirectory": "node_modules/tslint-microsoft-contrib", - "rules": { - "class-name": true, - "curly": true, - "eofline": true, - "mocha-avoid-only": true, - "indent": [ - true, - "tabs" - ], - "interface-name": true, - "jsdoc-format": true, - "max-line-length": [ - false, - 140 - ], - "prefer-const": true, - "no-consecutive-blank-lines": true, - "no-construct": true, - "no-debugger": true, - "no-duplicate-variable": true, - "no-shadowed-variable": true, - "no-empty": true, - "no-eval": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unused-expression": true, - "no-use-before-declare": true, - "no-var-keyword": true, - "no-var-requires": false, - "one-line": [ - true, - "check-catch", - "check-finally", - "check-else", - "check-open-brace", - "check-whitespace" - ], - "no-floating-promises": true, - "quotemark": [ - false, - "double" - ], - "semicolon": true, - "space-before-function-paren": false, - "switch-default": false, - "trailing-comma": [ - false, - { - "multiline": "always", - "singleline": "always" - } - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "use-isnan": true, - "variable-name": [ - true, - "ban-keywords", - "allow-leading-underscore" - ], - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-module", - "check-separator" - ] - } -} \ No newline at end of file diff --git a/packages/doctor/typings/interfaces.ts b/packages/doctor/typings/interfaces.d.ts similarity index 100% rename from packages/doctor/typings/interfaces.ts rename to packages/doctor/typings/interfaces.d.ts diff --git a/packages/doctor/typings/nativescript-doctor.d.ts b/packages/doctor/typings/nativescript-doctor.d.ts index b0bbc8f08b..580919f7a0 100644 --- a/packages/doctor/typings/nativescript-doctor.d.ts +++ b/packages/doctor/typings/nativescript-doctor.d.ts @@ -1,4 +1,4 @@ -/// +/// declare module "@nativescript/doctor" { export const doctor: NativeScriptDoctor.IDoctor; diff --git a/packages/doctor/vitest.config.ts b/packages/doctor/vitest.config.ts new file mode 100644 index 0000000000..92ca35aaf5 --- /dev/null +++ b/packages/doctor/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +// Unlike the CLI, this package has no injector doing constructor-source +// reflection, so the TypeScript sources run directly with no build step. +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["test/**/*.ts"], + exclude: ["**/node_modules/**", "**/*.d.ts"], + }, +}); diff --git a/postinstall.js b/postinstall.js index 5d3e0d3e25..1efef2b89e 100644 --- a/postinstall.js +++ b/postinstall.js @@ -2,9 +2,14 @@ var child_process = require("child_process"); var path = require("path"); -var constants = require(path.join(__dirname, "lib", "constants")); +var fs = require("fs"); +// In the published package dist/ is the root; in a source checkout the compiled +// output lives under dist/ while lib/ holds only TypeScript. +var distLib = path.join(__dirname, "dist", "lib"); +var pathToLib = fs.existsSync(distLib) ? distLib : path.join(__dirname, "lib"); +var constants = require(path.join(pathToLib, "constants")); var commandArgs = [path.join(__dirname, "bin", "tns"), constants.POST_INSTALL_COMMAND_NAME]; -var helpers = require(path.join(__dirname, "lib", "common", "helpers")); +var helpers = require(path.join(pathToLib, "common", "helpers")); if (helpers.isInstallingNativeScriptGlobally()) { child_process.spawn(process.argv[0], commandArgs, { stdio: "inherit" }); } diff --git a/resources/test/example.vitest.js b/resources/test/example.vitest.js new file mode 100644 index 0000000000..fc30d00960 --- /dev/null +++ b/resources/test/example.vitest.js @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest'; + +describe('example', () => { + it('runs on the device', () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/resources/test/example.vitest.ts b/resources/test/example.vitest.ts new file mode 100644 index 0000000000..d512dc5bf0 --- /dev/null +++ b/resources/test/example.vitest.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from "vitest"; + +describe("example", () => { + it("runs on the device", () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/resources/test/network_security.xml b/resources/test/network_security.xml new file mode 100644 index 0000000000..96fb169145 --- /dev/null +++ b/resources/test/network_security.xml @@ -0,0 +1,10 @@ + + + + + localhost + 127.0.0.1 + 10.0.2.2 + + diff --git a/resources/test/test-main.vitest.js b/resources/test/test-main.vitest.js new file mode 100644 index 0000000000..6c5da46172 --- /dev/null +++ b/resources/test/test-main.vitest.js @@ -0,0 +1,19 @@ +import '@valor/nativescript-websockets'; +import { Application } from '@nativescript/core'; +import { + NativeScriptVitestCoordinator, + createWebpackTestRegistry, +} from '@nativescript/unit-test-runner/runtime'; +import { createVitestHostPage } from '@nativescript/unit-test-runner/testing'; + +const coordinator = new NativeScriptVitestCoordinator({ + // Every spec matched by the Vitest `include` patterns must also be matched + // here, or the device will not be able to load it. + registry: createWebpackTestRegistry( + require.context('./', true, /\.spec\.js$/) + ), +}); + +void coordinator.start(); +// The host page keeps the screen free as a mount() surface for UI specs. +Application.run({ create: () => createVitestHostPage(coordinator) }); diff --git a/resources/test/test-main.vitest.ts b/resources/test/test-main.vitest.ts new file mode 100644 index 0000000000..d15c5c9db0 --- /dev/null +++ b/resources/test/test-main.vitest.ts @@ -0,0 +1,21 @@ +import "@valor/nativescript-websockets"; +import { Application } from "@nativescript/core"; +import { + NativeScriptVitestCoordinator, + createWebpackTestRegistry, +} from "@nativescript/unit-test-runner/runtime"; +import { createVitestHostPage } from "@nativescript/unit-test-runner/testing"; + +declare let require: any; + +const coordinator = new NativeScriptVitestCoordinator({ + // Every spec matched by the Vitest `include` patterns must also be matched + // here, or the device will not be able to load it. + registry: createWebpackTestRegistry( + require.context("./", true, /\.spec\.ts$/), + ), +}); + +void coordinator.start(); +// The host page keeps the screen free as a mount() surface for UI specs. +Application.run({ create: () => createVitestHostPage(coordinator) }); diff --git a/resources/test/vitest.config.mts b/resources/test/vitest.config.mts new file mode 100644 index 0000000000..51dec4f8aa --- /dev/null +++ b/resources/test/vitest.config.mts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config'; +import { nativeScript } from '@nativescript/unit-test-runner'; + +// Platform is selected per run: `ns test ios` / `ns test android` / +// `ns test visionos`, or NS_PLATFORM=ios npx vitest run +export default defineConfig({ + plugins: [ + nativeScript({ + platform: process.env.NS_PLATFORM || 'ios', // 'android' | 'ios' | 'visionos' + device: process.env.NS_DEVICE || undefined, + }), + ], + test: { + // Device runs include app startup and real layout passes. + testTimeout: 30_000, + }, +}); diff --git a/scripts/build-docs.js b/scripts/build-docs.js new file mode 100644 index 0000000000..2c4900ab51 --- /dev/null +++ b/scripts/build-docs.js @@ -0,0 +1,51 @@ +const fs = require("fs"); +const path = require("path"); +const _ = require("lodash"); + +const rootDir = path.join(__dirname, ".."); +const sourceDir = path.join(rootDir, "docs", "man_pages"); +const outputDir = path.join(rootDir, "docs-cli"); + +const templateData = { + isJekyll: true, + isHtml: true, + isConsole: true, + isWindows: true, + isMacOS: true, + isLinux: true, + constants: "", +}; + +function* markdownFiles(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + yield* markdownFiles(entryPath); + } else if (entry.name.endsWith(".md")) { + yield entryPath; + } + } +} + +// output from a man page that was since deleted or renamed would otherwise +// linger and ship as stale documentation +fs.rmSync(outputDir, { recursive: true, force: true }); + +for (const sourcePath of markdownFiles(sourceDir)) { + const outputPath = path.join( + outputDir, + path.relative(sourceDir, sourcePath) + ); + + let rendered; + try { + rendered = _.template(fs.readFileSync(sourcePath, "utf8"))(templateData); + } catch (err) { + throw new Error( + `Failed to render ${path.relative(rootDir, sourcePath)}: ${err.message}` + ); + } + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, rendered); +} diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 0000000000..4879322f88 --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,37 @@ +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const rootDir = path.join(__dirname, ".."); + +fs.rmSync(path.join(rootDir, "dist"), { recursive: true, force: true }); + +// tsc never removes output whose source is gone, so every build starts from an +// empty dist - otherwise a deleted file keeps being compiled-in and tested +// against, which is the failure this whole layout exists to prevent. +if (process.argv.includes("--dist-only")) { + process.exit(0); +} + +// Builds used to emit next to each source file, so a tree that predates dist/ +// still has hundreds of stale .js lying around. .gitignore is the source of +// truth for which of those are compiler output - its negations protect the +// vendored, hook and fixture .js that must survive. +const result = child_process.spawnSync("git", ["clean", "-Xdf", "lib", "test"], { + cwd: rootDir, + stdio: "inherit", +}); + +if (result.error) { + throw result.error; +} + +if (result.status !== 0) { + throw new Error(`git clean exited with status ${result.status}`); +} + +for (const entry of fs.readdirSync(rootDir)) { + if (entry.endsWith(".tgz")) { + fs.rmSync(path.join(rootDir, entry)); + } +} diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js new file mode 100644 index 0000000000..284bf5fdff --- /dev/null +++ b/scripts/copy-assets.js @@ -0,0 +1,150 @@ +const fs = require("fs"); +const path = require("path"); + +// dist/ is assembled as a complete package root, not just compiled output: +// lib/ resolves siblings through __dirname (../package.json, ../docs/helpers, +// ../../vendor/gradle-plugin, ...), so those directories have to sit next to it +// exactly as they do in the repo. + +const rootDir = path.join(__dirname, ".."); +const distDir = path.join(rootDir, "dist"); +const release = process.argv.includes("--release"); + +const SIBLING_DIRS = [ + "resources", + "docs", + "config", + "vendor", + "bin", + "setup", + "contracts", +]; +// npm picks README/LICENSE/CHANGELOG up from the directory being packed, so +// they have to exist inside dist or they silently drop out of the tarball +const ROOT_FILES = [ + "postinstall.js", + "preuninstall.js", + "README.md", + "LICENSE", + "CHANGELOG.md", +]; + +// paths (relative to the repo root) that never ship +const RELEASE_EXCLUDES = [ + path.join("docs", "html"), + path.join("lib", "common", "docs", "fonts"), + path.join("lib", "common", "test"), +]; + +function isExcluded(relPath) { + if (!release) { + return false; + } + return RELEASE_EXCLUDES.some( + (excluded) => + relPath === excluded || relPath.startsWith(excluded + path.sep), + ); +} + +let copied = 0; +let skipped = 0; + +function copyFile(sourcePath, targetPath) { + const source = fs.statSync(sourcePath); + if (fs.existsSync(targetPath)) { + const target = fs.statSync(targetPath); + if (target.mtimeMs >= source.mtimeMs && target.size === source.size) { + skipped++; + return; + } + } + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.copyFileSync(sourcePath, targetPath); + copied++; +} + +function copyTree(relDir, filter) { + const sourceDir = path.join(rootDir, relDir); + if (!fs.existsSync(sourceDir)) { + return; + } + + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { + const relPath = path.join(relDir, entry.name); + if (isExcluded(relPath)) { + continue; + } + if (entry.isDirectory()) { + copyTree(relPath, filter); + } else if (!filter || filter(relPath)) { + copyFile(path.join(rootDir, relPath), path.join(distDir, relPath)); + } + } +} + +// Everything under lib/ that is not TypeScript is an asset: vendored scripts, +// hooks, platform-tools binaries, docs helpers and test fixtures. A .js with a +// sibling .ts is compiler output instead - either left over from the old +// in-place build or freshly emitted into dist - and copying it would overwrite +// what tsc just produced. +function isCompilerOutput(relPath) { + const stem = relPath.replace(/\.js\.map$/, "").replace(/\.js$/, ""); + return ( + (relPath.endsWith(".js") || relPath.endsWith(".js.map")) && + fs.existsSync(path.join(rootDir, stem + ".ts")) + ); +} + +// Hand-written .d.ts come along too. tsc treats them as inputs and never emits +// them, but the generated declarations import from them, so leaving them behind +// ships types with dangling references. +copyTree( + "lib", + (relPath) => + (!relPath.endsWith(".ts") || relPath.endsWith(".d.ts")) && + !isCompilerOutput(relPath), +); + +for (const dir of SIBLING_DIRS) { + copyTree(dir); +} + +if (!release) { + // fixtures the compiled tests read relative to their own location + copyTree(path.join("test", "files")); +} + +for (const file of ROOT_FILES) { + copyFile(path.join(rootDir, file), path.join(distDir, file)); +} + +writeManifest(); + +function writeManifest() { + const pkg = JSON.parse( + fs.readFileSync(path.join(rootDir, "package.json"), "utf8"), + ); + + // dist is the package root once published, so entrypoints lose the dist/ + // prefix they carry in the source manifest + pkg.main = pkg.main.replace(/^\.\/dist\//, "./"); + + delete pkg.devDependencies; + delete pkg.files; + delete pkg.overrides; + delete pkg["lint-staged"]; + + pkg.scripts = { + postinstall: pkg.scripts.postinstall, + preuninstall: pkg.scripts.preuninstall, + }; + + fs.writeFileSync( + path.join(distDir, "package.json"), + JSON.stringify(pkg, null, 2) + "\n", + ); +} + +console.log( + `assets: ${copied} copied, ${skipped} up to date${release ? " (release)" : ""}`, +); diff --git a/scripts/generate-test-deps.js b/scripts/generate-test-deps.js new file mode 100644 index 0000000000..e4a5396827 --- /dev/null +++ b/scripts/generate-test-deps.js @@ -0,0 +1,41 @@ +const fs = require("fs"); +const path = require("path"); +const { manifest } = require("pacote"); + +const configsBasePath = path.join(__dirname, "..", "config"); +const dependenciesPath = path.join(configsBasePath, "test-dependencies.json"); +const generatedVersionFilePath = path.join( + configsBasePath, + "test-deps-versions-generated.json" +); + +async function latestVersion(name) { + // only fetches the package.json for the latest dist-tag + const { version } = await manifest(name.toLowerCase(), { + fullMetadata: false, + }); + return version; +} + +async function main() { + const testDependencies = JSON.parse( + fs.readFileSync(dependenciesPath, "utf8") + ); + + const dependenciesVersions = {}; + for (const dep of testDependencies) { + dependenciesVersions[dep.name] = dep.version || (await latestVersion(dep.name)); + } + + fs.writeFileSync( + generatedVersionFilePath, + JSON.stringify(dependenciesVersions, null, 2) + ); + + console.log("Wrote", generatedVersionFilePath); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/get-next-version.js b/scripts/get-next-version.js index 5f6be10cbf..7eb8784a79 100644 --- a/scripts/get-next-version.js +++ b/scripts/get-next-version.js @@ -37,7 +37,9 @@ let lastTagVersion = ( .stdout.toString() ) .trim() - .substring(1); + // git describe emits the tag verbatim (v9.1.0); LAST_TAGGED_VERSION callers + // pass a bare version, which for scoped packages has no "v" to strip + .replace(/^v/, ""); if (!semver.parse(lastTagVersion)) { throw new Error("Invalid last tag version"); } diff --git a/scripts/guard-root-pack.js b/scripts/guard-root-pack.js new file mode 100644 index 0000000000..521cd96130 --- /dev/null +++ b/scripts/guard-root-pack.js @@ -0,0 +1,19 @@ +// The published package is assembled in dist/ and packed from there, so packing +// the repository root would produce a tarball with everything nested one level +// deeper - every path the CLI resolves through __dirname would break, silently. +// npm pack ./dist runs dist's own manifest, so this guard does not fire for it. +// +// The release script is deliberately not called "pack": npm runs pre for +// any script, so `npm run pack` would fire this guard as its own pre-hook. +console.error( + [ + "Refusing to pack the repository root.", + "", + "The published package is assembled in dist/. Use:", + " npm run pack.release", + "", + "which builds dist/ and runs `npm pack ./dist`.", + ].join("\n") +); + +process.exit(1); diff --git a/scripts/set-ga-id.js b/scripts/set-ga-id.js new file mode 100644 index 0000000000..1d367e670e --- /dev/null +++ b/scripts/set-ga-id.js @@ -0,0 +1,70 @@ +const fs = require("fs"); +const path = require("path"); +const EOL = require("os").EOL; + +// a real environment always wins over .env, so CI secrets are never shadowed +require("dotenv").config({ + path: path.join(__dirname, "..", ".env"), + quiet: true, +}); + +// GA4 measurement ids (G-XXXXXXXXXX). Leaving these empty disables analytics in +// the produced build - the provider skips every hit when either value is unset. +const GA_MEASUREMENT_IDS = { + dev: "", + live: "G-T4P12SN9HJ", +}; + +// The api secret pairs with the measurement id and is a credential, so it is +// read from the environment at release time rather than committed here. +const API_SECRET_ENV = { + dev: "GA_API_SECRET_DEV", + live: "GA_API_SECRET", +}; + +const MEASUREMENT_ID_KEY = "GA_MEASUREMENT_ID"; +const API_SECRET_KEY = "GA_API_SECRET"; +const rootDir = path.join(__dirname, ".."); + +// Releases flip the id inside dist/ rather than in the working tree, so a failed +// pack cannot leave a checkout configured to report as production. +const dirIndex = process.argv.indexOf("--dir"); +const baseDir = + dirIndex === -1 ? rootDir : path.resolve(rootDir, process.argv[dirIndex + 1]); +const configPath = path.join(baseDir, "config", "config.json"); + +function readConfig() { + return JSON.parse(fs.readFileSync(configPath, "utf8")); +} + +const mode = process.argv[2]; + +if (mode === "verify") { + const config = readConfig(); + + if (!GA_MEASUREMENT_IDS.live) { + console.warn( + `Warning: no GA4 measurement id is configured in ${__filename}, so this build reports no analytics.` + ); + } else if (config[MEASUREMENT_ID_KEY] !== GA_MEASUREMENT_IDS.live) { + throw new Error( + `Google Analytics measurement id is not configured correctly in ${configPath}` + ); + } else if (!config[API_SECRET_KEY]) { + // not fatal: the provider skips every hit without it, so the build is sound + // and merely reports nothing - the same state a release ships today + console.warn( + `Warning: $${API_SECRET_ENV.live} is not set, so this build reports no analytics.` + ); + } +} else if (mode === "live" || mode === "dev") { + const config = readConfig(); + config[MEASUREMENT_ID_KEY] = GA_MEASUREMENT_IDS[mode]; + config[API_SECRET_KEY] = process.env[API_SECRET_ENV[mode]] || ""; + fs.writeFileSync(configPath, JSON.stringify(config, null, "\t") + EOL); +} else { + console.error( + "Usage: node scripts/set-ga-id.js [--dir ]" + ); + process.exit(1); +} diff --git a/test/.mocharc.yml b/test/.mocharc.yml deleted file mode 100644 index 86b280d432..0000000000 --- a/test/.mocharc.yml +++ /dev/null @@ -1,61 +0,0 @@ -# --recursive -# --reporter spec -# --require source-map-support/register -# --require test/test-bootstrap.js -# --timeout 150000 -# test/ -# lib/common/test/unit-tests - -# This is an example Mocha config containing every Mocha option plus others -allow-uncaught: false -async-only: false -bail: false -check-leaks: false -color: true -delay: false -diff: true -exit: true # could be expressed as "no-exit: true" -extension: - - 'js' -# fgrep and grep are mutually exclusive -# fgrep: something -# file: -# - '/path/to/some/file' -# - '/path/to/some/other/file' -forbid-only: false -forbid-pending: false -full-trace: false -# global: -# - 'jQuery' -# - '$' -# fgrep and grep are mutually exclusive -# grep: something -growl: false -# ignore: -# - '/path/to/some/ignored/file' -inline-diffs: false -# needs to be used with grep or fgrep -# invert: false -recursive: true -reporter: 'spec' -require: - - 'test/test-bootstrap.js' -retries: 1 -slow: 500 -sort: false -spec: - - 'test/**/*.js' - - 'lib/common/test/unit-tests/**/*.js' -timeout: 150000 # same as "no-timeout: true" or "timeout: 0" - -# node flags -# trace-warnings: true - -ui: 'bdd' -v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-" -watch: false -watch-files: - - 'test/**/*.js' - - 'lib/common/test/unit-tests/**/*.js' -# watch-ignore: -# - 'lib/vendor' diff --git a/test/android-tools-info.ts b/test/android-tools-info.ts index 4db963b43f..39324051f5 100644 --- a/test/android-tools-info.ts +++ b/test/android-tools-info.ts @@ -39,18 +39,54 @@ describe("androidToolsInfo", () => { return testInjector; }; + describe("getCompileSdkVersion", () => { + const resolveCompileSdk = ( + compileSdk: number, + installedTargets: string[], + ): number => { + const testInjector = createTestInjector(); + testInjector.register("options", { compileSdk }); + const androidToolsInfo: any = testInjector.resolve(AndroidToolsInfo); + return androidToolsInfo.getCompileSdkVersion(installedTargets, 36); + }; + + it("accepts a user-specified compile sdk matching an exact installed target", () => { + assert.equal(resolveCompileSdk(36, ["android-35", "android-36"]), 36); + }); + + it("accepts a user-specified compile sdk installed as a minor-versioned target", () => { + assert.equal( + resolveCompileSdk(37, ["android-36", "android-37.0", "android-37.1"]), + 37, + ); + }); + + it("fails when the user-specified compile sdk is not installed", () => { + assert.throws( + () => resolveCompileSdk(38, ["android-36", "android-37.0"]), + "You have specified '38' for compile sdk, but it is not installed on your system.", + ); + }); + + it("does not treat extension targets as the base platform", () => { + assert.throws( + () => resolveCompileSdk(35, ["android-34", "android-35-ext15"]), + "You have specified '35' for compile sdk, but it is not installed on your system.", + ); + }); + }); + describe("validateJavacVersion", () => { it("throws error when passing showWarningsAsErrors to true and javac is not installed", () => { const testInjector = createTestInjector(); - const androidToolsInfo = testInjector.resolve( - AndroidToolsInfo - ); + const androidToolsInfo = + testInjector.resolve(AndroidToolsInfo); assert.throws( () => androidToolsInfo.validateJavacVersion(null, { showWarningsAsErrors: true, }), - "Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable." + "Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", ); }); }); diff --git a/test/command-registration.ts b/test/command-registration.ts new file mode 100644 index 0000000000..6b497cc7f1 --- /dev/null +++ b/test/command-registration.ts @@ -0,0 +1,83 @@ +import { assert } from "chai"; +import { Yok } from "../lib/common/yok"; + +const noopCommandFactory = () => ({ + execute: async (): Promise => undefined, +}); + +describe("yok: command registration", () => { + let injector: Yok; + + beforeEach(() => { + injector = new Yok(); + }); + + describe("registerCommand with a hierarchical name", () => { + it("records the subcommand under its parent", () => { + injector.registerCommand("dev|test", noopCommandFactory); + + assert.deepStrictEqual(injector.getChildrenCommandsNames("dev"), [ + "test", + ]); + }); + + it("routes arguments through the parent name", () => { + injector.registerCommand("dev|test", noopCommandFactory); + + const built = injector.buildHierarchicalCommand("dev", ["test", "extra"]); + + assert.deepStrictEqual(built, { + commandName: "dev|test", + remainingArguments: ["extra"], + }); + }); + + it("synthesizes a dispatcher for the parent", () => { + injector.registerCommand("dev|test", noopCommandFactory); + + const parent = injector.resolveCommand("dev"); + + assert.isTrue((parent).isHierarchicalCommand); + }); + + it("records each sibling once, including default commands", () => { + injector.registerCommand("dev|*test", noopCommandFactory); + injector.registerCommand("dev|lint", noopCommandFactory); + + assert.deepStrictEqual(injector.getChildrenCommandsNames("dev"), [ + "*test", + "lint", + ]); + }); + + it("does not duplicate a subcommand already recorded by requireCommand", () => { + injector.requireCommand("dev|test", "some-file"); + injector.registerCommand("dev|test", noopCommandFactory); + + assert.deepStrictEqual(injector.getChildrenCommandsNames("dev"), [ + "test", + ]); + }); + }); + + describe("register with a zero-parameter arrow factory", () => { + it("resolves by calling the factory, without annotate()", () => { + const factory = () => ({ value: 42 }); + injector.register("arrowFactoryService", factory); + + const instance = injector.resolve("arrowFactoryService"); + + assert.strictEqual(instance.value, 42); + assert.isUndefined((factory).$inject); + }); + + it("stays shared by default", () => { + injector.register("sharedArrowFactoryService", () => ({})); + + assert.strictEqual( + injector.resolve("sharedArrowFactoryService"), + injector.resolve("sharedArrowFactoryService"), + ); + }); + }); +}); diff --git a/test/commands-service.ts b/test/commands-service.ts new file mode 100644 index 0000000000..0bd644fdee --- /dev/null +++ b/test/commands-service.ts @@ -0,0 +1,66 @@ +import { assert } from "chai"; +import { Yok } from "../lib/common/yok"; +import { CommandsService } from "../lib/common/services/commands-service"; +import { ICommand } from "../lib/common/definitions/commands"; + +function createTestInjector(command: ICommand): { + injector: Yok; + validatedWith: { called: boolean }; +} { + const injector = new Yok(); + const validatedWith = { called: false }; + + injector.register("errors", { + fail: (message: string): void => { + throw new Error(message); + }, + failWithHelp: (message: string): void => { + throw new Error(message); + }, + }); + injector.register("hooksService", {}); + injector.register("logger", { warn: (): void => undefined }); + injector.register("options", { + validateOptions: (): void => { + validatedWith.called = true; + }, + }); + injector.register("staticConfig", {}); + injector.register("extensibilityService", {}); + injector.register("optionsTracker", {}); + + injector.resolveCommand = () => command; + + return { injector, validatedWith }; +} + +describe("commands-service", () => { + describe("option validation", () => { + const baseCommand: ICommand = { + execute: async (): Promise => undefined, + allowedParameters: [], + canExecute: async (): Promise => true, + }; + + it("validates the options of an ordinary command", async () => { + const { injector, validatedWith } = createTestInjector(baseCommand); + const service = injector.resolve(CommandsService); + + await (service).tryExecuteCommandAction("info", []); + + assert.isTrue(validatedWith.called); + }); + + it("skips validation for a command that forwards its options", async () => { + const { injector, validatedWith } = createTestInjector({ + ...baseCommand, + skipOptionsValidation: true, + }); + const service = injector.resolve(CommandsService); + + await (service).tryExecuteCommandAction("preview", []); + + assert.isFalse(validatedWith.called); + }); + }); +}); diff --git a/test/compat/injector-facade-surface.ts b/test/compat/injector-facade-surface.ts new file mode 100644 index 0000000000..96f4e3c522 --- /dev/null +++ b/test/compat/injector-facade-surface.ts @@ -0,0 +1,131 @@ +import { assert } from "chai"; +import { Yok, getInjector } from "../../lib/common/yok"; +import { Injector, inject, runInInjectionContext } from "../../lib/common/di"; +import { + CommandRegistry, + KeyCommandRegistry, + ModuleRegistry, + PublicApiBuilder, +} from "../../lib/common/contracts"; + +// Pins the externally reachable injector surface: every IInjector member +// (lib/common/definitions/yok.d.ts) plus dispose, subclassability, the +// injector self-registration, and the global assignment. The Yok facade must +// keep all of these behaving through the DI migration. + +const FACADE_METHODS = [ + "require", + "requirePublic", + "requirePublicClass", + "requireCommand", + "requireKeyCommand", + "resolve", + "resolveCommand", + "resolveKeyCommand", + "register", + "registerCommand", + "registerKeyCommand", + "getRegisteredCommandsNames", + "getRegisteredKeyCommandsNames", + "dynamicCall", + "getDynamicCallData", + "isDefaultCommand", + "isValidHierarchicalCommand", + "getChildrenCommandsNames", + "buildHierarchicalCommand", + "dispose", +]; + +describe("injector facade surface", () => { + it("exposes every IInjector member", () => { + const inj: any = new Yok(); + for (const member of FACADE_METHODS) { + assert.isFunction(inj[member], `missing facade method: ${member}`); + } + assert.instanceOf(inj.dynamicCallRegex, RegExp); + assert.isObject(inj.publicApi); + assert.isObject(inj.publicApi.__modules__); + assert.isBoolean(inj.overrideAlreadyRequiredModule); + }); + + it("registers itself under 'injector', resolvable with and without the $ prefix", () => { + const inj = new Yok(); + assert.strictEqual(inj.resolve("injector"), inj); + assert.strictEqual(inj.resolve("$injector"), inj); + }); + + it("remains subclassable (the InjectorStub pattern in test/stubs.ts)", () => { + class SubInjector extends Yok {} + const sub = new SubInjector(); + sub.register("subclassed", { value: 42 }); + assert.equal(sub.resolve("subclassed").value, 42); + assert.strictEqual(sub.resolve("injector"), sub); + }); + + it("keeps getInjector() synchronized with a direct global.$injector assignment", () => { + const previous = getInjector(); + const fresh = new Yok(); + + (global).$injector = fresh; + try { + assert.strictEqual(getInjector(), fresh); + } finally { + (global).$injector = previous; + } + assert.strictEqual(getInjector(), previous); + }); + + it("assigns the process-wide global.$injector", () => { + assert.isOk((global).$injector); + for (const member of FACADE_METHODS) { + assert.isFunction( + (global).$injector[member], + `global.$injector missing: ${member}`, + ); + } + }); + + it("IS an Injector: instanceof holds and the new API works on the facade directly", () => { + const inj = new Yok(); + assert.instanceOf(inj, Injector); + + // Provider-form registration dispatches to the container... + inj.register({ provide: "viaProvider", useValue: { tag: 1 } }); + assert.equal(inj.get("viaProvider").tag, 1); + // ...while string-form registration keeps legacy semantics. + inj.register("viaLegacy", { tag: 2 }); + assert.equal(inj.resolve("viaLegacy").tag, 2); + assert.strictEqual(inj.get("viaLegacy"), inj.resolve("viaLegacy")); + + // One identity: the injection context IS the facade. + runInInjectionContext(inj, () => { + assert.strictEqual(inject(Injector), inj); + }); + }); + + it("registers its subsystem faces as tokens that resolve to the facade", () => { + const inj = new Yok(); + + for (const token of [ + CommandRegistry, + KeyCommandRegistry, + ModuleRegistry, + PublicApiBuilder, + ]) { + assert.strictEqual(inj.get(token), inj); + } + assert.strictEqual(inj.resolve("commandRegistry"), inj); + + runInInjectionContext(inj, () => { + assert.strictEqual(inject(CommandRegistry), inj); + }); + }); + + it("calls lowercase/anonymous resolvers as factories instead of new-ing them", () => { + const inj = new Yok(); + inj.register("factoryMade", function () { + return { madeByFactory: true }; + }); + assert.isTrue(inj.resolve("factoryMade").madeByFactory); + }); +}); diff --git a/test/compat/legacy-extension.ts b/test/compat/legacy-extension.ts new file mode 100644 index 0000000000..42acdbd0da --- /dev/null +++ b/test/compat/legacy-extension.ts @@ -0,0 +1,110 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { ICliGlobal } from "../../lib/common/definitions/cli-global"; + +// Pins the published extension contract: an extension is require()d and +// registers its contributions by mutating global.$injector — commands via +// requireCommand/registerCommand (lazy, path-based), services via register. +// extending-cli.md advertises this surface. + +const cliGlobal = (global); + +describe("legacy extension contract", () => { + let extDir: string; + let capture: any; + + beforeEach(() => { + extDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-compat-ext-")); + capture = (global).__extCapture = {}; + }); + + afterEach(() => { + fs.rmSync(extDir, { recursive: true, force: true }); + delete (global).__extCapture; + }); + + it("an unmodified extension contributes services and hierarchical commands via global.$injector", async () => { + const commandPath = path.join(extDir, "meow-command"); + fs.writeFileSync( + commandPath + ".js", + `class MeowPurrCommand { + constructor($extCompatService) { + this.extCompatService = $extCompatService; + this.allowedParameters = []; + } + async execute(args) { + global.__extCapture.executedWith = args; + global.__extCapture.service = this.extCompatService; + } + } + global.$injector.registerCommand("meowcompat|purr", MeowPurrCommand);`, + ); + fs.writeFileSync( + path.join(extDir, "main.js"), + `global.$injector.register("extCompatService", { name: "ext-compat-service" }); + global.$injector.requireCommand("meowcompat|purr", ${JSON.stringify( + commandPath, + )});`, + ); + + require(path.join(extDir, "main.js")); + + // Registration is lazy: the command module must not load until resolved. + assert.isUndefined(capture.executedWith); + + // Services registered by the extension resolve under both spellings. + const service = cliGlobal.$injector.resolve("extCompatService"); + assert.strictEqual( + cliGlobal.$injector.resolve("$extCompatService"), + service, + ); + assert.equal(service.name, "ext-compat-service"); + + // The command participates in the hierarchical router. + assert.include( + cliGlobal.$injector.getRegisteredCommandsNames(false), + "meowcompat|purr", + ); + assert.include( + cliGlobal.$injector.getChildrenCommandsNames("meowcompat"), + "purr", + ); + const built = cliGlobal.$injector.buildHierarchicalCommand("meowcompat", [ + "purr", + "extra-arg", + ]); + assert.equal(built.commandName, "meowcompat|purr"); + assert.deepEqual(built.remainingArguments, ["extra-arg"]); + + // The leaf command resolves, gets its DI-injected service, and executes. + const command = cliGlobal.$injector.resolveCommand("meowcompat|purr"); + assert.isOk(command); + await command.execute(["fluffy"]); + assert.deepEqual(capture.executedWith, ["fluffy"]); + assert.equal(capture.service.name, "ext-compat-service"); + + // registerCommand on a hierarchical name synthesized a parent dispatcher. + const parent = cliGlobal.$injector.resolveCommand("meowcompat"); + assert.isTrue(parent.isHierarchicalCommand); + }); + + it("claiming an already-required command name throws unless overrideAlreadyRequiredModule is set", () => { + const firstPath = path.join(extDir, "first"); + fs.writeFileSync(firstPath + ".js", `module.exports = {};`); + + cliGlobal.$injector.requireCommand("conflictcompat", firstPath); + assert.throws( + () => cliGlobal.$injector.requireCommand("conflictcompat", firstPath), + /require'd twice/, + ); + + cliGlobal.$injector.overrideAlreadyRequiredModule = true; + try { + cliGlobal.$injector.requireCommand("conflictcompat", firstPath); + } finally { + cliGlobal.$injector.overrideAlreadyRequiredModule = false; + } + }); +}); diff --git a/test/compat/legacy-hooks.ts b/test/compat/legacy-hooks.ts new file mode 100644 index 0000000000..ed8c1d9f66 --- /dev/null +++ b/test/compat/legacy-hooks.ts @@ -0,0 +1,345 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { Yok, getInjector, setGlobalInjector } from "../../lib/common/yok"; +import { HooksService } from "../../lib/common/services/hooks-service"; +import { hook } from "../../lib/common/helpers"; +import { IInjector } from "../../lib/common/definitions/yok"; +import { IHooksService } from "../../lib/common/declarations"; +import { LoggerStub, ErrorsStub } from "../stubs"; + +// Pins the published third-party hook contract: hooks are plain JS files whose +// exported function is resolved by its own parameter names (`$logger`, +// `hookArgs`, ...). Payload shapes and influence channels covered here are the +// compatibility bar for any DI changes. + +function createTestInjector(projectDir: string): IInjector { + const testInjector = new Yok(); + testInjector.register("logger", LoggerStub); + testInjector.register("errors", ErrorsStub); + testInjector.register("fs", { + exists: (p: string) => fs.existsSync(p), + getFsStats: (p: string) => fs.statSync(p), + readDirectory: (p: string) => fs.readdirSync(p), + readText: (p: string) => fs.readFileSync(p, "utf8"), + }); + testInjector.register("childProcess", {}); + testInjector.register("config", { DISABLE_HOOKS: false }); + testInjector.register("staticConfig", { + CLIENT_NAME: "tns", + version: "0.0.0", + }); + testInjector.register("projectHelper", { projectDir }); + testInjector.register("options", { hooks: true }); + testInjector.register("performanceService", { + now: () => 0, + processExecutionData: () => { + /* not measured here */ + }, + }); + testInjector.register("projectConfigService", { + getValue: (_key: string, defaultValue: any) => defaultValue, + }); + testInjector.register("projectData", { fromContainer: true }); + testInjector.register("hooksService", HooksService); + return testInjector; +} + +function writeHook(projectDir: string, hookName: string, source: string): void { + const hooksDir = path.join(projectDir, "hooks"); + fs.mkdirSync(hooksDir, { recursive: true }); + fs.writeFileSync(path.join(hooksDir, `${hookName}.js`), source); +} + +describe("legacy hook contract", () => { + let projectDir: string; + let testInjector: IInjector; + let capture: any; + + const hooksService = (): IHooksService => + testInjector.resolve("hooksService"); + const logger = (): LoggerStub => testInjector.resolve("logger"); + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-compat-hooks-")); + testInjector = createTestInjector(projectDir); + capture = (global).__hookCapture = {}; + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + delete (global).__hookCapture; + }); + + it("injects services by $-prefixed parameter name and passes hookArgs by identity", async () => { + writeHook( + projectDir, + "before-case1", + `module.exports = function ($logger, $injector, hookArgs) { + global.__hookCapture.logger = $logger; + global.__hookCapture.injector = $injector; + global.__hookCapture.hookArgs = hookArgs; + };`, + ); + + const payload = { anything: 1 }; + await hooksService().executeBeforeHooks("case1", { hookArgs: payload }); + + assert.strictEqual(capture.logger, testInjector.resolve("logger")); + assert.strictEqual(capture.injector, testInjector); + assert.strictEqual(capture.hookArgs, payload); + assert.include(logger().traceOutput, "hooks.param-name-signature"); + }); + + it("derives the hook name from the pre-| part of hierarchical command names", async () => { + writeHook( + projectDir, + "before-case2", + `module.exports = function (hookArgs) { global.__hookCapture.ran = true; };`, + ); + + await hooksService().executeBeforeHooks("case2|android", { + hookArgs: {}, + }); + + assert.isTrue(capture.ran); + }); + + it("promotes hookArgs.projectData so it shadows the container's projectData under both spellings", async () => { + writeHook( + projectDir, + "after-case3", + `module.exports = function ($projectData, projectData) { + global.__hookCapture.dollar = $projectData; + global.__hookCapture.plain = projectData; + };`, + ); + + const payloadProjectData = { projectDir, fromPayload: true }; + await hooksService().executeAfterHooks("case3", { + hookArgs: { projectData: payloadProjectData }, + }); + + assert.strictEqual(capture.dollar, payloadProjectData); + assert.strictEqual(capture.plain, payloadProjectData); + }); + + it("lets a hook mutate the payload in a way the caller observes (before-build-task-args channel)", async () => { + writeHook( + projectDir, + "before-build-task-args", + `module.exports = function (hookArgs) { hookArgs.args.push("--offline"); };`, + ); + + const args = ["assembleDebug"]; + await hooksService().executeBeforeHooks("build-task-args", { + hookArgs: { args }, + }); + + assert.deepEqual(args, ["assembleDebug", "--offline"]); + // A hookArgs-only signature is the recommended pattern and must not be + // reported as param-name injection. + assert.notInclude(logger().traceOutput, "hooks.param-name-signature"); + }); + + it("treats a rejection carrying stopExecution + errorAsWarning as a warning, not a failure", async () => { + writeHook( + projectDir, + "before-case5", + `module.exports = async function () { + const err = new Error("abort-as-warning"); + err.stopExecution = false; + err.errorAsWarning = true; + throw err; + };`, + ); + + await hooksService().executeBeforeHooks("case5"); + + assert.include(logger().warnOutput, "abort-as-warning"); + }); + + it("fails command execution when a hook rejects without errorAsWarning", async () => { + writeHook( + projectDir, + "before-case6", + `module.exports = async function () { throw new Error("hard-abort"); };`, + ); + + await assert.isRejected( + hooksService().executeBeforeHooks("case6"), + /hard-abort/, + ); + }); + + it("runs hooks with no payload at all (every command name is a hook point)", async () => { + writeHook( + projectDir, + "before-case7", + `module.exports = function ($logger) { global.__hookCapture.ran = true; };`, + ); + + await hooksService().executeBeforeHooks("case7"); + + assert.isTrue(capture.ran); + }); + + it("skips (with a warning) a hook whose parameters name unwrapped payload keys", async () => { + // after-watchAction-style payloads pass keys at the top level with no + // hookArgs wrapper. validateHookArguments only consults the container, so + // a hook naming such a key is skipped as invalid — today's behavior, which + // the migration must preserve exactly. + writeHook( + projectDir, + "after-case8", + `module.exports = function (liveSyncResultInfo) { global.__hookCapture.ran = true; };`, + ); + + await hooksService().executeAfterHooks("case8", { + liveSyncResultInfo: { fake: true }, + }); + + assert.isUndefined(capture.ran); + assert.include(logger().warnOutput, "invalid arguments"); + }); + + it("folds a function returned by a before-hook into a middleware chain around the @hook-decorated method", async () => { + writeHook( + projectDir, + "before-case9", + `module.exports = function (hookArgs) { + return function (args, originalMethod) { + global.__hookCapture.middlewareArgs = args.slice(); + return originalMethod.apply(null, args).then(function (result) { + return "wrapped(" + result + ")"; + }); + }; + };`, + ); + + class Subject { + constructor(public $hooksService: IHooksService) {} + + @hook("case9") + async doWork(input: string): Promise { + (global).__hookCapture.originalRan = true; + return "original:" + input; + } + } + + const subject = testInjector.resolve(Subject); + const result = await subject.doWork("x"); + + assert.equal(result, "wrapped(original:x)"); + assert.isTrue(capture.originalRan); + assert.deepEqual(capture.middlewareArgs, ["x"]); + }); + + it("lets a middleware short-circuit so the original method never runs", async () => { + writeHook( + projectDir, + "before-case10", + `module.exports = function () { + return function (args, originalMethod) { + return "short-circuited"; + }; + };`, + ); + + class Subject { + constructor(public $hooksService: IHooksService) {} + + @hook("case10") + async doWork(): Promise { + (global).__hookCapture.originalRan = true; + return "original"; + } + } + + const subject = testInjector.resolve(Subject); + const result = await subject.doWork(); + + assert.equal(result, "short-circuited"); + assert.isUndefined(capture.originalRan); + }); + + it("runs hook bodies in an injection context, so inject() resolves services", async () => { + const diPath = require.resolve("../../lib/common/di"); + writeHook( + projectDir, + "before-case-inject", + `const { inject, Injector } = require(${JSON.stringify(diPath)}); + module.exports = function (hookArgs) { + global.__hookCapture.logger = inject("logger"); + global.__hookCapture.container = inject(Injector); + global.__hookCapture.hookArgs = hookArgs; + };`, + ); + + const payload = { sample: true }; + await hooksService().executeBeforeHooks("case-inject", { + hookArgs: payload, + }); + + assert.strictEqual(capture.logger, testInjector.resolve("logger")); + assert.strictEqual(capture.container, testInjector); + assert.strictEqual(capture.hookArgs, payload); + }); + + it("@hook falls back to the global injector when the class has neither $hooksService nor $injector", async () => { + writeHook( + projectDir, + "before-case11", + `module.exports = function () { global.__hookCapture.ran = true; };`, + ); + + class Subject { + @hook("case11") + async doWork(): Promise { + return "ok"; + } + } + + const previousInjector = getInjector(); + setGlobalInjector(testInjector); + try { + const result = await new Subject().doWork(); + assert.equal(result, "ok"); + assert.isTrue(capture.ran); + } finally { + setGlobalInjector(previousInjector); + } + }); + + it("@hook prefers the instance's $injector over the global injector", async () => { + writeHook( + projectDir, + "before-case12", + `module.exports = function () { global.__hookCapture.ran = true; };`, + ); + + class Subject { + public $injector = testInjector; + + @hook("case12") + async doWork(): Promise { + return "ok"; + } + } + + const previousInjector = getInjector(); + setGlobalInjector({ + resolve: () => { + throw new Error("the process-wide injector must be the last resort"); + }, + }); + try { + const result = await new Subject().doWork(); + assert.equal(result, "ok"); + assert.isTrue(capture.ran); + } finally { + setGlobalInjector(previousInjector); + } + }); +}); diff --git a/test/config/config-json.ts b/test/config/config-json.ts index 9697b64339..18af169af2 100644 --- a/test/config/config-json.ts +++ b/test/config/config-json.ts @@ -6,7 +6,9 @@ describe("config.json", () => { ANDROID_DEBUG_UI_MAC: "Google Chrome", USE_POD_SANDBOX: false, DISABLE_HOOKS: false, - GA_TRACKING_ID: "UA-111455-51", + // a checkout reports nothing; scripts/set-ga-id.js fills these in dist/ + GA_MEASUREMENT_ID: "", + GA_API_SECRET: "", }; it("validates content is correct", () => { @@ -14,7 +16,7 @@ describe("config.json", () => { assert.deepStrictEqual( data, expectedData, - "Data in config.json is not correct. Is this expected?" + "Data in config.json is not correct. Is this expected?", ); }); }); diff --git a/test/contracts.ts b/test/contracts.ts new file mode 100644 index 0000000000..e07581e01e --- /dev/null +++ b/test/contracts.ts @@ -0,0 +1,132 @@ +import { assert } from "chai"; +import { getContractName, Injector, provide } from "../lib/common/di"; +import type { InjectionToken, ProviderToken } from "../lib/common/di"; +import { + ChildProcess, + DevicesService, + DoctorService, + Errors, + FileSystem, + HostInfo, + HttpClient, + Logger, + PackageManager, + ProjectData, + ProjectDataService, + ProjectNameService, + Prompter, + TempService, + ViteHmrPortService, + PBXPROJ_DOM_XCODE, + XCODE, +} from "../lib/contracts"; +import { Yok } from "../lib/common/yok"; +import { Logger as LoggerImpl } from "../lib/common/logger/logger"; +import { Errors as ErrorsImpl } from "../lib/common/errors"; +import { FileSystem as FileSystemImpl } from "../lib/common/file-system"; +import type { ISpawnResult } from "../lib/common/declarations"; + +const tranche: [ProviderToken, string][] = [ + [ChildProcess, "childProcess"], + [DevicesService, "devicesService"], + [DoctorService, "doctorService"], + [Errors, "errors"], + [FileSystem, "fs"], + [HostInfo, "hostInfo"], + [HttpClient, "httpClient"], + [Logger, "logger"], + [PackageManager, "packageManager"], + [ProjectData, "projectData"], + [ProjectDataService, "projectDataService"], + [ProjectNameService, "projectNameService"], + [Prompter, "prompter"], + [TempService, "tempService"], + [ViteHmrPortService, "viteHmrPortService"], +]; + +describe("contracts tranche", () => { + it("carries decorator-set token names matching the Yok-era registrations", () => { + for (const [token, legacyName] of tranche) { + assert.equal(getContractName(token), legacyName); + } + }); + + it("resolves via the class, the legacy name, and the $-spelling to one instance", () => { + class StubDoctorService extends DoctorService { + async printWarnings(): Promise {} + async runSetupScript(): Promise { + return {}; + } + async canExecuteLocalBuild(): Promise { + return true; + } + checkForDeprecatedShortImportsInAppDir(): void {} + } + + const injector = new Injector([provide(DoctorService, StubDoctorService)]); + + const byClass = injector.get(DoctorService); + assert.instanceOf(byClass, StubDoctorService); + assert.strictEqual(injector.get("doctorService"), byClass); + assert.strictEqual(injector.get("$doctorService"), byClass); + }); + + for (const [token, legacyName] of tranche) { + it(`aliases '${legacyName}' by class, by name and by $-spelling`, () => { + const instance = {}; + const injector = new Injector([{ provide: token, useValue: instance }]); + + assert.strictEqual(injector.get(token), instance); + assert.strictEqual(injector.get(legacyName), instance); + assert.strictEqual(injector.get(`$${legacyName}`), instance); + }); + } + + it("finds registrations made under the legacy name only", () => { + const instance = {}; + const injector = new Injector([ + { provide: "tempService", useValue: instance }, + ]); + + assert.strictEqual(injector.get(TempService), instance); + }); + + describe("injection tokens", () => { + const tokens: [InjectionToken, string][] = [ + [XCODE, "xcode"], + [PBXPROJ_DOM_XCODE, "pbxprojDomXcode"], + ]; + + for (const [token, legacyName] of tokens) { + it(`aliases '${legacyName}' by token, by name and by $-spelling`, () => { + assert.equal(token.description, legacyName); + + // The registration these tokens alias is a module namespace object + // made by `injector.register(name, module)` under lib/node/. + const moduleValue = {}; + const injector = new Injector([ + { provide: legacyName, useValue: moduleValue }, + ]); + + assert.strictEqual(injector.get(token), moduleValue); + assert.strictEqual(injector.get(legacyName), moduleValue); + assert.strictEqual(injector.get(`$${legacyName}`), moduleValue); + }); + } + }); + + describe("against a real Yok container", () => { + it("resolves the token and the legacy name to the same instance", () => { + const injector = new Yok(); + injector.register("config", { DEBUG: false }); + injector.register("logger", LoggerImpl); + injector.register("errors", ErrorsImpl); + injector.register("fs", FileSystemImpl); + + assert.instanceOf(injector.get(Logger), LoggerImpl); + assert.strictEqual(injector.get(Logger), injector.resolve("logger")); + assert.strictEqual(injector.get(Errors), injector.resolve("errors")); + assert.strictEqual(injector.get(FileSystem), injector.resolve("fs")); + }); + }); +}); diff --git a/test/controllers/debug-controller.ts b/test/controllers/debug-controller.ts index 03595c67bb..c5bac5bc85 100644 --- a/test/controllers/debug-controller.ts +++ b/test/controllers/debug-controller.ts @@ -36,7 +36,7 @@ const defaultDeviceIdentifier = "Nexus5"; class PlatformDebugService extends EventEmitter /* implements IPlatformDebugService */ { public async debug( debugData: IDebugData, - debugOptions: IDebugOptions + debugOptions: IDebugOptions, ): Promise { return { debugUrl: fakeChromeDebugUrl }; } @@ -63,7 +63,7 @@ interface IDebugTestData { } const getDefaultDeviceInformation = ( - platform?: string + platform?: string, ): IDebugTestDeviceInfo => ({ deviceInfo: { status: constants.CONNECTED_STATUS, @@ -86,7 +86,7 @@ const getDefaultTestData = (platform?: string): IDebugTestData => ({ describe("debugController", () => { const getTestInjectorForTestConfiguration = ( - testData: IDebugTestData + testData: IDebugTestData, ): IInjector => { const testInjector = new Yok(); testInjector.register("devicesService", { @@ -97,12 +97,12 @@ describe("debugController", () => { applicationManager: { isApplicationInstalled: async ( - appIdentifier: string + appIdentifier: string, ): Promise => testData.isApplicationInstalledOnDevice, }, isEmulator: testData.deviceInformation.isEmulator, - } + } : null; }, }); @@ -145,13 +145,14 @@ describe("debugController", () => { testInjector.register("prepareDataService", PrepareDataService); testInjector.register("prepareNativePlatformService", {}); testInjector.register("projectDataService", ProjectDataService); + testInjector.register("projectData", stubs.ProjectDataStub); testInjector.register("fs", {}); testInjector.register("staticConfig", StaticConfig); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("androidResourcesMigrationService", {}); testInjector.register( "liveSyncProcessDataService", - LiveSyncProcessDataService + LiveSyncProcessDataService, ); return testInjector; @@ -170,7 +171,7 @@ describe("debugController", () => { const assertIsRejected = async ( testData: IDebugTestData, expectedError: string, - userSpecifiedOptions?: IDebugOptions + userSpecifiedOptions?: IDebugOptions, ): Promise => { const testInjector = getTestInjectorForTestConfiguration(testData); const debugController = testInjector.resolve(DebugController); @@ -178,7 +179,7 @@ describe("debugController", () => { const debugData = getDebugData(); await assert.isRejected( debugController.startDebug(debugData, userSpecifiedOptions), - expectedError + expectedError, ); }; @@ -196,7 +197,7 @@ describe("debugController", () => { await assertIsRejected( testData, - "is unreachable. Make sure it is Trusted " + "is unreachable. Make sure it is Trusted ", ); }); @@ -206,7 +207,7 @@ describe("debugController", () => { await assertIsRejected( testData, - "is not installed on device with identifier" + "is not installed on device with identifier", ); }); @@ -216,12 +217,12 @@ describe("debugController", () => { await assertIsRejected( testData, - DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING + DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING, ); }); const assertIsRejectedWhenPlatformDebugServiceFails = async ( - platform: string + platform: string, ): Promise => { const testData = getDefaultTestData(); testData.deviceInformation.deviceInfo.platform = platform; @@ -229,11 +230,11 @@ describe("debugController", () => { const testInjector = getTestInjectorForTestConfiguration(testData); const expectedErrorMessage = "Platform specific error"; const platformDebugService = testInjector.resolve( - `${platform}DeviceDebugService` + `${platform}DeviceDebugService`, ); platformDebugService.debug = async ( data: IDebugData, - debugOptions: IDebugOptions + debugOptions: IDebugOptions, ): Promise => { throw new Error(expectedErrorMessage); }; @@ -243,7 +244,7 @@ describe("debugController", () => { const debugData = getDebugData(); await assert.isRejected( debugController.startDebug(debugData, null), - expectedErrorMessage + expectedErrorMessage, ); }; @@ -277,16 +278,17 @@ describe("debugController", () => { message: "my message", code: 2048, }; - const platformDebugService = testInjector.resolve< - IDeviceDebugService - >(`${platform}DeviceDebugService`); + const platformDebugService = + testInjector.resolve( + `${platform}DeviceDebugService`, + ); platformDebugService.emit( CONNECTION_ERROR_EVENT_NAME, - expectedErrorData + expectedErrorData, ); assert.deepStrictEqual( dataRaisedForConnectionError, - expectedErrorData + expectedErrorData, ); }); }); @@ -338,12 +340,11 @@ describe("debugController", () => { testData.deviceInformation.deviceInfo.platform = "iOS"; const testInjector = getTestInjectorForTestConfiguration(testData); - const analyticsService = testInjector.resolve( - "analyticsService" - ); + const analyticsService = + testInjector.resolve("analyticsService"); let dataTrackedToGA: IEventActionData = null; analyticsService.trackEventActionInGoogleAnalytics = async ( - data: IEventActionData + data: IEventActionData, ): Promise => { dataTrackedToGA = data; }; @@ -351,11 +352,10 @@ describe("debugController", () => { const debugController = testInjector.resolve(DebugController); const debugData = getDebugData(testCase.debugOptions); await debugController.startDebug(debugData); - const devicesService = testInjector.resolve( - "devicesService" - ); + const devicesService = + testInjector.resolve("devicesService"); const device = devicesService.getDeviceByIdentifier( - testData.deviceInformation.deviceInfo.identifier + testData.deviceInformation.deviceInfo.identifier, ); const expectedData = JSON.stringify( @@ -366,16 +366,16 @@ describe("debugController", () => { projectDir: debugData.projectDir, }, null, - 2 + 2, ); // Use JSON.stringify as the compared objects link to new instances of different classes. assert.deepStrictEqual( JSON.stringify(dataTrackedToGA, null, 2), - expectedData + expectedData, ); }); - } + }, ); }); }); diff --git a/test/controllers/run-controller.ts b/test/controllers/run-controller.ts index 36251a12aa..c09eb73f12 100644 --- a/test/controllers/run-controller.ts +++ b/test/controllers/run-controller.ts @@ -69,13 +69,12 @@ function getFullSyncResult(): ILiveSyncResultInfo { } function mockDevicesService(injector: IInjector, devices: Mobile.IDevice[]) { - const devicesService: Mobile.IDevicesService = injector.resolve( - "devicesService" - ); + const devicesService: Mobile.IDevicesService = + injector.resolve("devicesService"); devicesService.execute = async ( action: (device: Mobile.IDevice) => Promise, canExecute?: (dev: Mobile.IDevice) => boolean, - options?: { allowNoDevices?: boolean } + options?: { allowNoDevices?: boolean }, ) => { for (const d of devices) { if (canExecute(d)) { @@ -132,12 +131,18 @@ function createTestInjector() { injector.register("debugController", {}); injector.register("liveSyncProcessDataService", LiveSyncProcessDataService); injector.register("tempService", TempServiceStub); + injector.register("staticConfig", { + getAdbFilePath: async () => "adb", + }); + injector.register("viteHmrPortService", { + getPort: async () => 5173, + }); const devicesService = injector.resolve("devicesService"); devicesService.getDevicesForPlatform = () => [{ identifier: "myTestDeviceId1" }]; devicesService.getPlatformsFromDeviceDescriptors = ( - devices: ILiveSyncDeviceDescriptor[] + devices: ILiveSyncDeviceDescriptor[], ) => devices.map((d) => map[d.identifier].device.deviceInfo.platform); devicesService.on = () => ({}); @@ -206,20 +211,17 @@ describe("RunController", () => { describe("watch", () => { const testCases = [ { - name: - "should prepare only ios platform when only ios devices are connected", + name: "should prepare only ios platform when only ios devices are connected", connectedDevices: [iOSDeviceDescriptor], expectedPreparedPlatforms: ["ios"], }, { - name: - "should prepare only android platform when only android devices are connected", + name: "should prepare only android platform when only android devices are connected", connectedDevices: [androidDeviceDescriptor], expectedPreparedPlatforms: ["android"], }, { - name: - "should prepare both platforms when ios and android devices are connected", + name: "should prepare both platforms when ios and android devices are connected", connectedDevices: [iOSDeviceDescriptor, androidDeviceDescriptor], expectedPreparedPlatforms: ["ios", "android"], }, @@ -229,15 +231,14 @@ describe("RunController", () => { it(testCase.name, async () => { mockDevicesService( injector, - testCase.connectedDevices.map((d) => map[d.identifier].device) + testCase.connectedDevices.map((d) => map[d.identifier].device), ); const preparedPlatforms: string[] = []; - const prepareController: PrepareController = injector.resolve( - "prepareController" - ); + const prepareController: PrepareController = + injector.resolve("prepareController"); prepareController.prepare = async ( - currentPrepareData: PrepareData + currentPrepareData: PrepareData, ) => { preparedPlatforms.push(currentPrepareData.platform); return { @@ -253,7 +254,7 @@ describe("RunController", () => { assert.deepStrictEqual( preparedPlatforms, - testCase.expectedPreparedPlatforms + testCase.expectedPreparedPlatforms, ); }); }); @@ -263,41 +264,35 @@ describe("RunController", () => { describe("stopRunOnDevices", () => { const testCases = [ { - name: - "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers", + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers", currentDeviceIdentifiers: ["device1", "device2", "device3"], expectedDeviceIdentifiers: ["device1", "device2", "device3"], }, { - name: - "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers (when a single device is attached)", + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers (when a single device is attached)", currentDeviceIdentifiers: ["device1"], expectedDeviceIdentifiers: ["device1"], }, { - name: - "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them (when a single device is attached)", + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them (when a single device is attached)", currentDeviceIdentifiers: ["device1"], expectedDeviceIdentifiers: ["device1"], deviceIdentifiersToBeStopped: ["device1"], }, { - name: - "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them", + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them", currentDeviceIdentifiers: ["device1", "device2", "device3"], expectedDeviceIdentifiers: ["device1", "device3"], deviceIdentifiersToBeStopped: ["device1", "device3"], }, { - name: - "does not raise liveSyncStopped event for device, which is not currently being liveSynced", + name: "does not raise liveSyncStopped event for device, which is not currently being liveSynced", currentDeviceIdentifiers: ["device1", "device2", "device3"], expectedDeviceIdentifiers: ["device1"], deviceIdentifiersToBeStopped: ["device1", "device4"], }, { - name: - "stops LiveSync operation for all devices when stop method is called with empty array", + name: "stops LiveSync operation for all devices when stop method is called with empty array", currentDeviceIdentifiers: ["device1", "device2", "device3"], expectedDeviceIdentifiers: ["device1", "device2", "device3"], deviceIdentifiersToBeStopped: [], @@ -307,14 +302,14 @@ describe("RunController", () => { for (const testCase of testCases) { it(testCase.name, async () => { const liveSyncProcessDataService = injector.resolve( - "liveSyncProcessDataService" + "liveSyncProcessDataService", ); (liveSyncProcessDataService).persistData( projectDir, testCase.currentDeviceIdentifiers.map( - (identifier) => { identifier } + (identifier) => { identifier }, ), - ["ios"] + ["ios"], ); const emittedDeviceIdentifiersForLiveSyncStoppedEvent: string[] = []; @@ -322,7 +317,7 @@ describe("RunController", () => { runController.on(RunOnDeviceEvents.runOnDeviceStopped, (data: any) => { assert.equal(data.projectDir, projectDir); emittedDeviceIdentifiersForLiveSyncStoppedEvent.push( - data.deviceIdentifier + data.deviceIdentifier, ); }); @@ -333,7 +328,7 @@ describe("RunController", () => { assert.deepStrictEqual( emittedDeviceIdentifiersForLiveSyncStoppedEvent, - testCase.expectedDeviceIdentifiers + testCase.expectedDeviceIdentifiers, ); }); } diff --git a/test/controllers/update-controller.ts b/test/controllers/update-controller.ts index a99cf9802d..2037d970aa 100644 --- a/test/controllers/update-controller.ts +++ b/test/controllers/update-controller.ts @@ -20,6 +20,7 @@ function createTestInjector(projectDir: string = projectFolder): IInjector { initializeProjectData: () => { /* empty */ }, + getBuildRelativeDirectoryPath: () => "platforms", dependencies: { "@nativescript/core": "next", }, diff --git a/test/define-command.ts b/test/define-command.ts new file mode 100644 index 0000000000..f249669849 --- /dev/null +++ b/test/define-command.ts @@ -0,0 +1,985 @@ +import { assert } from "chai"; +import { spawnSync } from "child_process"; +import * as path from "path"; +import { Yok } from "../lib/common/yok"; +import { IInjector } from "../lib/common/definitions/yok"; +import { inject } from "../lib/common/di"; +import { CommandRegistry } from "../lib/common/contracts/command-registry"; +import { CommandsService } from "../lib/common/services/commands-service"; +import { Options } from "../lib/options"; +import { Errors } from "../lib/common/errors"; +import { LoggerStub, HooksServiceStub } from "./stubs"; +import { + arrayOption, + booleanOption, + defineCommand, + isCommandDefinition, + numberOption, + stringOption, +} from "../lib/common/define-command"; +import { + createCommandFromDefinition, + registerCommandDefinition, +} from "../lib/common/services/command-definition-adapter"; + +const createTestInjector = (options: any = {}): IInjector => { + const testInjector = new Yok(); + testInjector.register("options", options); + testInjector.register("logger", LoggerStub); + testInjector.register("errors", { + failWithHelp: (message: string) => { + throw new Error(message); + }, + }); + return testInjector; +}; + +describe("defineCommand", () => { + it("marks definitions so a duplicated CLI copy still recognises them", () => { + const definition = defineCommand({ + name: "dctest-marker", + run: (): void => undefined, + }); + + assert.isTrue(isCommandDefinition(definition)); + assert.isTrue( + (definition)[Symbol.for("nativescript:cli:commandDefinition")], + ); + assert.isFalse(isCommandDefinition({ name: "dctest-marker" })); + assert.isFalse(isCommandDefinition(null)); + }); + + it("keeps the marker on a spread-derived copy", () => { + const derived = { + ...defineCommand({ name: "dctest-spread", run: (): void => undefined }), + name: "dctest-spread-derived", + }; + + assert.isTrue(isCommandDefinition(derived)); + }); + + describe("define-time validation", () => { + const rejects = (definition: any, expected: RegExp) => + assert.throws(() => defineCommand(definition), expected); + + it("names the command and the accepted form in every message", () => { + rejects( + { name: "dctest-bad", run: 42 }, + /Invalid command definition for 'dctest-bad'.*'run' must be a function.*Accepted form: defineCommand/s, + ); + }); + + it("rejects a missing or unusable name", () => { + rejects( + { run: (): void => undefined }, + /an unnamed command.*'name' must be/s, + ); + rejects({ name: "", run: (): void => undefined }, /'name' must be/); + rejects({ name: [], run: (): void => undefined }, /'name' must be/); + rejects( + { name: ["ok", ""], run: (): void => undefined }, + /'name' must be/, + ); + rejects({ name: 7, run: (): void => undefined }, /'name' must be/); + }); + + it("rejects a missing run", () => { + rejects({ name: "dctest-norun" }, /'run' must be a function/); + }); + + it("rejects a typo'd definition field", () => { + rejects( + { + name: "dctest-typo", + handler: (): void => undefined, + run: (): void => undefined, + }, + /unknown field\(s\) 'handler'/, + ); + }); + + it("rejects an unusable arguments policy", () => { + rejects( + { name: "dctest-args", arguments: "one", run: (): void => undefined }, + /'arguments' is 'one'; it must be "none" or "any"/, + ); + }); + + it("rejects a non-function canExecute and non-boolean flags", () => { + rejects( + { name: "dctest-can", canExecute: true, run: (): void => undefined }, + /'canExecute' must be a function/, + ); + rejects( + { + name: "dctest-flag", + disableAnalytics: "yes", + run: (): void => undefined, + }, + /'disableAnalytics' must be a boolean/, + ); + rejects( + { name: "dctest-flag2", enableHooks: 1, run: (): void => undefined }, + /'enableHooks' must be a boolean/, + ); + }); + + it("rejects an option with an unsupported type", () => { + rejects( + { + name: "dctest-opt", + options: { verbose: { type: "bool" } }, + run: (): void => undefined, + }, + /option 'verbose' has type 'bool'; the supported types are boolean, string, number, array/, + ); + }); + + it("rejects an option that is not a spec at all", () => { + rejects( + { + name: "dctest-opt2", + options: { verbose: true }, + run: (): void => undefined, + }, + /option 'verbose' must be declared with one of booleanOption/, + ); + }); + + it("rejects a typo'd option-spec field", () => { + rejects( + { + name: "dctest-opt3", + options: { verbose: { type: "boolean", describe: "no" } }, + run: (): void => undefined, + }, + /option 'verbose' has unknown field\(s\) 'describe'/, + ); + }); + + it("rejects unusable alias, hasSensitiveValue and description entries", () => { + rejects( + { + name: "dctest-opt4", + options: { verbose: { type: "boolean", alias: 1 } }, + run: (): void => undefined, + }, + /option 'verbose' declares an 'alias'/, + ); + rejects( + { + name: "dctest-opt5", + options: { verbose: { type: "boolean", hasSensitiveValue: "yes" } }, + run: (): void => undefined, + }, + /non-boolean 'hasSensitiveValue'/, + ); + rejects( + { + name: "dctest-opt6", + options: { verbose: { type: "boolean", description: 5 } }, + run: (): void => undefined, + }, + /non-string 'description'/, + ); + }); + + it("accepts every documented field", () => { + assert.doesNotThrow(() => + defineCommand({ + name: ["dctest-full", "dctest-full-alias"], + description: "Everything at once", + options: { + verbose: booleanOption({ default: false }), + output: stringOption({ alias: ["o", "out"], description: "Dir" }), + retries: numberOption({ default: 1 }), + files: arrayOption({ hasSensitiveValue: true }), + }, + arguments: "any", + canExecute: () => true, + disableAnalytics: true, + enableHooks: false, + run: (): void => undefined, + }), + ); + }); + }); + + describe("option value types", () => { + it("types default-less options as possibly undefined", () => { + // The repo builds without strictNullChecks, which erases the very + // `| undefined` under test, so the assertions live in their own + // strict project. + const project = path.join( + __dirname, + "..", + "..", + "test", + "type-fixtures", + "tsconfig.json", + ); + const result = spawnSync( + process.execPath, + [require.resolve("typescript/bin/tsc"), "-p", project], + { encoding: "utf8" }, + ); + + assert.strictEqual( + result.status, + 0, + `${result.stdout || ""}${result.stderr || ""}`, + ); + }); + }); + + describe("registration", () => { + it("round-trips through the legacy command registry", () => { + const definition = defineCommand({ + name: "dctestwidget|add", + description: "Adds a widget", + run: (): void => undefined, + }); + + const testInjector = createTestInjector(); + registerCommandDefinition(definition, testInjector); + + const command = testInjector.resolveCommand("dctestwidget|add"); + assert.isFunction(command.execute); + assert.deepEqual(command.allowedParameters, []); + + const parent = testInjector.resolveCommand("dctestwidget"); + assert.isTrue(parent.isHierarchicalCommand); + + assert.include( + testInjector.getRegisteredCommandsNames(false), + "dctestwidget|add", + ); + }); + + it("caches one command instance per registered name", () => { + const testInjector = createTestInjector(); + registerCommandDefinition( + defineCommand({ name: "dctestflat", run: (): void => undefined }), + testInjector, + ); + + assert.strictEqual( + testInjector.resolveCommand("dctestflat"), + testInjector.resolveCommand("dctestflat"), + ); + }); + + it("registers every alias of a multi-name definition", () => { + const testInjector = createTestInjector(); + registerCommandDefinition( + defineCommand({ + name: ["dctestalias", "dctestalias2"], + run: (): void => undefined, + }), + testInjector, + ); + + assert.isFunction(testInjector.resolveCommand("dctestalias").execute); + assert.isFunction(testInjector.resolveCommand("dctestalias2").execute); + }); + + it("refuses a value that did not come from defineCommand", () => { + assert.throws( + () => + registerCommandDefinition( + { name: "dctestraw", run: (): void => undefined }, + createTestInjector(), + ), + /carries no command-definition marker/, + ); + }); + + it("registers through the CommandRegistry the target injector provides", () => { + const testInjector = createTestInjector(); + const registered: string[] = []; + testInjector.register({ + provide: CommandRegistry, + useValue: { + registerCommand: (name: string) => registered.push(name), + }, + }); + + registerCommandDefinition( + defineCommand({ + name: ["dctestfacet", "dctestfacet2"], + run: (): void => undefined, + }), + testInjector, + ); + + assert.deepEqual(registered, ["dctestfacet", "dctestfacet2"]); + assert.isNull(testInjector.resolveCommand("dctestfacet")); + }); + + it("keeps a registered command when a subcommand would shadow it", () => { + const testInjector = createTestInjector(); + registerCommandDefinition( + defineCommand({ name: "dctestowned", run: (): void => undefined }), + testInjector, + ); + + registerCommandDefinition( + defineCommand({ name: "dctestowned|sub", run: (): void => undefined }), + testInjector, + ); + + const owner = testInjector.resolveCommand("dctestowned"); + assert.isUndefined(owner.isHierarchicalCommand); + assert.isFunction(testInjector.resolveCommand("dctestowned|sub").execute); + + const logger: LoggerStub = testInjector.resolve("logger"); + assert.match( + logger.warnOutput, + /'dctestowned' is already registered as a command of its own.*'dctestowned\|sub' cannot be reached/, + ); + }); + }); + + describe("execute", () => { + it("passes args and the declared options through, inside an injection context", async () => { + const testInjector = createTestInjector({ + verbose: true, + output: "dist", + undeclared: "ignored", + }); + testInjector.register("dcTestGreeter", { greet: () => "hello" }); + + let capturedArgs: string[]; + let capturedOptions: any; + let greeting: string; + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestexec", + options: { + verbose: booleanOption(), + output: stringOption(), + }, + run(context) { + greeting = inject("dcTestGreeter").greet(); + capturedArgs = context.args; + capturedOptions = context.options; + }, + }), + testInjector, + ); + + await command.execute(["one", "two"]); + + assert.deepEqual(capturedArgs, ["one", "two"]); + assert.deepEqual(capturedOptions, { verbose: true, output: "dist" }); + assert.strictEqual(greeting, "hello"); + }); + + it("reads option values at execution time", async () => { + const optionsService: any = { verbose: false }; + const testInjector = createTestInjector(optionsService); + + let seen: boolean; + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestlate", + options: { verbose: booleanOption() }, + run: (context) => { + seen = context.options.verbose; + }, + }), + testInjector, + ); + + optionsService.verbose = true; + await command.execute([]); + + assert.isTrue(seen); + }); + + it("awaits an asynchronous run", async () => { + const testInjector = createTestInjector(); + let finished = false; + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestasync", + run: async () => { + await new Promise((resolve) => setTimeout(resolve, 1)); + finished = true; + }, + }), + testInjector, + ); + + await command.execute([]); + + assert.isTrue(finished); + }); + + it("carries the declared option values onto the run context", async () => { + const testInjector = createTestInjector({ + verbose: true, + output: "dist", + retries: 3, + files: ["a.ts"], + }); + + let seen: any; + const command = createCommandFromDefinition( + defineCommand({ + name: "dctesttypes", + options: { + verbose: booleanOption(), + output: stringOption(), + retries: numberOption(), + files: arrayOption(), + }, + run: (context) => { + seen = context.options; + }, + }), + testInjector, + ); + + await command.execute([]); + + assert.deepEqual(seen, { + verbose: true, + output: "dist", + retries: 3, + files: ["a.ts"], + }); + }); + }); + + describe("dashedOptions", () => { + it("compiles the schema into the shape the option parser expects", () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestdashed", + options: { + verbose: booleanOption({ default: false }), + output: stringOption({ alias: "o" }), + retries: numberOption({ default: 3 }), + files: arrayOption(), + token: stringOption({ + hasSensitiveValue: true, + description: "Auth token", + }), + }, + run: (): void => undefined, + }), + createTestInjector(), + ); + + assert.deepEqual(command.dashedOptions, { + verbose: { type: "boolean", hasSensitiveValue: false, default: false }, + output: { type: "string", hasSensitiveValue: false, alias: "o" }, + retries: { type: "number", hasSensitiveValue: false, default: 3 }, + files: { type: "array", hasSensitiveValue: false }, + token: { + type: "string", + hasSensitiveValue: true, + describe: "Auth token", + }, + }); + }); + + it("is empty when no options are declared", () => { + const command = createCommandFromDefinition( + defineCommand({ name: "dctestnoopts", run: (): void => undefined }), + createTestInjector(), + ); + + assert.deepEqual(command.dashedOptions, {}); + }); + + it("warns when a declared option or alias shadows a CLI-wide one", () => { + const testInjector = createTestInjector({ + options: { + verbose: { type: "boolean" }, + path: { type: "string", alias: "p" }, + }, + }); + + createCommandFromDefinition( + defineCommand({ + name: "dctestshadow", + options: { + verbose: booleanOption(), + output: stringOption({ alias: ["p", "o"] }), + fresh: booleanOption({ alias: "f" }), + }, + run: (): void => undefined, + }), + testInjector, + ); + + const logger: LoggerStub = testInjector.resolve("logger"); + assert.include( + logger.warnOutput, + "'--verbose' with the CLI option '--verbose'", + ); + assert.include( + logger.warnOutput, + "alias '-p' of '--output' with the CLI option '--path'", + ); + assert.notInclude(logger.warnOutput, "--fresh"); + assert.notInclude(logger.warnOutput, "'-o'"); + }); + + it("stays quiet when nothing collides", () => { + const testInjector = createTestInjector({ + options: { path: { type: "string", alias: "p" } }, + }); + + createCommandFromDefinition( + defineCommand({ + name: "dctestnoshadow", + options: { output: stringOption({ alias: ["o", "out"] }) }, + run: (): void => undefined, + }), + testInjector, + ); + + assert.strictEqual( + (testInjector.resolve("logger")).warnOutput, + "", + ); + }); + }); + + describe("canExecute", () => { + it("rejects positional arguments before consulting the definition", async () => { + let refined = false; + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestnone", + canExecute: () => { + refined = true; + return true; + }, + run: (): void => undefined, + }), + createTestInjector(), + ); + + await assert.isRejected( + command.canExecute(["stray"]), + /doesn't accept parameters/, + ); + assert.isFalse(refined); + assert.isTrue(await command.canExecute([])); + assert.isTrue(refined); + }); + + it("rejects positional arguments with no definition canExecute at all", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestnone2", + arguments: "none", + run: (): void => undefined, + }), + createTestInjector(), + ); + + await assert.isRejected( + command.canExecute(["stray"]), + /doesn't accept parameters/, + ); + assert.isTrue(await command.canExecute([])); + }); + + it("accepts anything when arguments are 'any'", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestany", + arguments: "any", + run: (): void => undefined, + }), + createTestInjector(), + ); + + assert.isTrue(await command.canExecute(["whatever", "else"])); + }); + + it("hands the context to a definition canExecute and honours its verdict", async () => { + const testInjector = createTestInjector({ force: true }); + let capturedContext: any; + + const build = (verdict: boolean) => + createCommandFromDefinition( + defineCommand({ + name: "dctestverdict", + arguments: "any", + options: { force: booleanOption() }, + canExecute: (context) => { + capturedContext = context; + return verdict; + }, + run: (): void => undefined, + }), + testInjector, + ); + + assert.isTrue(await build(true).canExecute(["android"])); + assert.deepEqual(capturedContext.args, ["android"]); + assert.deepEqual(capturedContext.options, { force: true }); + + assert.isFalse(await build(false).canExecute(["android"])); + }); + + it("runs the definition canExecute inside an injection context", async () => { + const testInjector = createTestInjector(); + testInjector.register("dcTestPolicy", { allowed: true }); + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestcaninject", + arguments: "any", + canExecute: () => inject("dcTestPolicy").allowed, + run: (): void => undefined, + }), + testInjector, + ); + + assert.isTrue(await command.canExecute(["anything"])); + }); + }); + + describe("ctx.fail", () => { + const createFailInjector = (): IInjector => { + const testInjector = createTestInjector(); + testInjector.register("errors", { + failWithHelp: (message: string) => { + throw new Error(`with help: ${message}`); + }, + }); + return testInjector; + }; + + it("fails the command from run, through failWithHelp", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestfailrun", + run: (ctx) => ctx.fail("no project found"), + }), + createFailInjector(), + ); + + await assert.isRejected( + command.execute([]), + /with help: no project found/, + ); + }); + + it("fails the command from canExecute, through failWithHelp", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestfailcan", + arguments: "any", + canExecute: (ctx) => + ctx.args.length === 1 || ctx.fail("expected one argument"), + run: (): void => undefined, + }), + createFailInjector(), + ); + + assert.isTrue(await command.canExecute(["one"])); + await assert.isRejected( + command.canExecute([]), + /with help: expected one argument/, + ); + }); + + it("rejects a message that carries nothing", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestfailempty", + run: (ctx) => ctx.fail(" "), + }), + createFailInjector(), + ); + + await assert.isRejected( + command.execute([]), + /ctx.fail\(\) for command 'dctestfailempty' requires a non-empty message/, + ); + }); + + it("still lets a thrown error through unchanged", async () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestthrow", + run: () => { + throw new Error("raw failure"); + }, + }), + createFailInjector(), + ); + + await assert.isRejected(command.execute([]), /^raw failure$/); + }); + }); + + describe("command flags", () => { + it("passes disableAnalytics and enableHooks through", () => { + const command = createCommandFromDefinition( + defineCommand({ + name: "dctestflags", + disableAnalytics: true, + enableHooks: false, + run: (): void => undefined, + }), + createTestInjector(), + ); + + assert.isTrue(command.disableAnalytics); + assert.isFalse(command.enableHooks); + }); + + it("leaves both absent when the definition omits them", () => { + const command = createCommandFromDefinition( + defineCommand({ name: "dctestnoflags", run: (): void => undefined }), + createTestInjector(), + ); + + assert.isFalse("disableAnalytics" in command); + assert.isFalse("enableHooks" in command); + }); + }); + + describe("option validation with the real options service", () => { + interface IValidationRun { + failures: string[]; + options: any; + } + + // The options service parses process.argv in its constructor, so each run + // gets its own injector and its own instance. + const validate = (definition: any, argv: string[]): IValidationRun => { + const failures: string[] = []; + const testInjector = new Yok(); + testInjector.register("staticConfig", { CLIENT_NAME: "" }); + testInjector.register("hostInfo", {}); + testInjector.register("settingsService", { + setSettings: (): any => undefined, + getProfileDir: () => "profileDir", + }); + testInjector.register("logger", LoggerStub); + + const errors = new Errors(testInjector); + errors.failWithHelp = ((message: string) => failures.push(message)); + errors.fail = ((message: string) => failures.push(message)); + testInjector.register("errors", errors); + testInjector.register("options", Options); + + const originalArgv = process.argv; + process.argv = [originalArgv[0], originalArgv[1], ...argv]; + try { + const command = createCommandFromDefinition(definition, testInjector); + const options: any = testInjector.resolve("options"); + options.validateOptions(command.dashedOptions); + return { failures, options }; + } finally { + process.argv = originalArgv; + } + }; + + beforeEach(() => { + process.env.NS_STRICT_OPTIONS = "error"; + }); + + afterEach(() => { + delete process.env.NS_STRICT_OPTIONS; + }); + + it("accepts an option declared with an array of aliases, under any spelling", () => { + const definition = defineCommand({ + name: "dctest-alias", + options: { outputDir: stringOption({ alias: ["o", "out"] }) }, + run: (): void => undefined, + }); + + for (const spelling of ["--output-dir", "--outputDir", "-o", "--out"]) { + const run = validate(definition, [spelling, "dist"]); + assert.deepEqual(run.failures, [], `rejected ${spelling}`); + assert.strictEqual(run.options.outputDir, "dist"); + } + }); + + it("still rejects an option the definition did not declare", () => { + const definition = defineCommand({ + name: "dctest-alias2", + options: { outputDir: stringOption({ alias: ["o", "out"] }) }, + run: (): void => undefined, + }); + + const run = validate(definition, ["--outputdirr", "dist"]); + + assert.lengthOf(run.failures, 1); + assert.match(run.failures[0], /'outputdirr' is not supported/); + }); + }); + + describe("end to end through CommandsService", () => { + let validatedOptions: any; + + const createCommandsServiceInjector = (options: any = {}): IInjector => { + const testInjector = new Yok(); + testInjector.register("errors", { + beginCommand: async (action: () => Promise) => action(), + failWithHelp: (message: string) => { + throw new Error(message); + }, + fail: (message: string) => { + throw new Error(message); + }, + }); + testInjector.register("hooksService", HooksServiceStub); + testInjector.register("logger", LoggerStub); + testInjector.register("staticConfig", { + disableAnalytics: true, + disableCommandHooks: true, + }); + testInjector.register("extensibilityService", {}); + testInjector.register("optionsTracker", {}); + testInjector.register("options", { + ...options, + validateOptions: (dashedOptions: any) => { + validatedOptions = dashedOptions; + }, + }); + testInjector.register("commandsService", CommandsService); + return testInjector; + }; + + beforeEach(() => { + validatedOptions = undefined; + }); + + it("validates the declared options and runs the command", async () => { + const testInjector = createCommandsServiceInjector({ verbose: true }); + let ran: any; + + registerCommandDefinition( + defineCommand({ + name: "dctest-e2e", + options: { verbose: booleanOption({ default: false }) }, + arguments: "any", + run: (context) => { + ran = context; + }, + }), + testInjector, + ); + + const commandsService: ICommandsService = + testInjector.resolve("commandsService"); + await commandsService.tryExecuteCommand("dctest-e2e", ["alpha"]); + + assert.deepEqual(validatedOptions, { + verbose: { type: "boolean", hasSensitiveValue: false, default: false }, + }); + assert.deepEqual(ran.args, ["alpha"]); + assert.deepEqual(ran.options, { verbose: true }); + }); + + it("rejects parameters when arguments are 'none'", async () => { + const testInjector = createCommandsServiceInjector(); + let ran = false; + + registerCommandDefinition( + defineCommand({ + name: "dctest-e2e-none", + run: () => { + ran = true; + }, + }), + testInjector, + ); + + const commandsService: ICommandsService = + testInjector.resolve("commandsService"); + await assert.isRejected( + commandsService.tryExecuteCommand("dctest-e2e-none", ["stray"]), + /doesn't accept parameters/, + ); + assert.isFalse(ran); + }); + + it("rejects parameters even when the definition supplies a canExecute", async () => { + const testInjector = createCommandsServiceInjector(); + let ran = false; + + registerCommandDefinition( + defineCommand({ + name: "dctest-e2e-refine", + canExecute: () => true, + run: () => { + ran = true; + }, + }), + testInjector, + ); + + const commandsService: ICommandsService = + testInjector.resolve("commandsService"); + await assert.isRejected( + commandsService.tryExecuteCommand("dctest-e2e-refine", ["stray"]), + /doesn't accept parameters/, + ); + assert.isFalse(ran); + }); + + it("dispatches a subcommand through the parent name", async () => { + const testInjector = createCommandsServiceInjector(); + let ran: any; + + registerCommandDefinition( + defineCommand({ + name: "dctest-widget|add", + arguments: "any", + run: (context) => { + ran = context; + }, + }), + testInjector, + ); + + const commandsService: ICommandsService = + testInjector.resolve("commandsService"); + await commandsService.tryExecuteCommand("dctest-widget", [ + "add", + "alpha", + ]); + + assert.deepEqual(ran.args, ["alpha"]); + }); + + it("dispatches the default subcommand, named or bare", async () => { + const testInjector = createCommandsServiceInjector(); + const runs: string[][] = []; + + registerCommandDefinition( + defineCommand({ + name: "dctest-gadget|*all", + arguments: "any", + run: (context) => { + runs.push(context.args); + }, + }), + testInjector, + ); + + const commandsService: ICommandsService = + testInjector.resolve("commandsService"); + await commandsService.tryExecuteCommand("dctest-gadget", ["all", "beta"]); + await commandsService.tryExecuteCommand("dctest-gadget", []); + + assert.deepEqual(runs, [["beta"], []]); + }); + }); +}); diff --git a/test/define-hook.ts b/test/define-hook.ts new file mode 100644 index 0000000000..22bac0974d --- /dev/null +++ b/test/define-hook.ts @@ -0,0 +1,515 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { Yok } from "../lib/common/yok"; +import { HooksService } from "../lib/common/services/hooks-service"; +import { hook } from "../lib/common/helpers"; +import { IInjector } from "../lib/common/definitions/yok"; +import { IHooksService } from "../lib/common/declarations"; +import { LoggerStub, ErrorsStub } from "./stubs"; +import { defineHook, isHookDefinition } from "../lib/common/define-hook"; + +// Hook fixtures load the API the way a real hook does — through the published +// `nativescript/contracts` entry point — so the marker symbol, the context +// shape and the hooks-service integration are exercised end to end. +const apiPath = require.resolve("../lib/contracts"); + +function createTestInjector(projectDir: string): IInjector { + const testInjector = new Yok(); + testInjector.register("logger", LoggerStub); + testInjector.register("errors", ErrorsStub); + testInjector.register("fs", { + exists: (p: string) => fs.existsSync(p), + getFsStats: (p: string) => fs.statSync(p), + readDirectory: (p: string) => fs.readdirSync(p), + readText: (p: string) => fs.readFileSync(p, "utf8"), + }); + testInjector.register("childProcess", {}); + testInjector.register("config", { DISABLE_HOOKS: false }); + testInjector.register("staticConfig", { + CLIENT_NAME: "tns", + version: "0.0.0", + }); + testInjector.register("projectHelper", { projectDir }); + testInjector.register("options", { hooks: true }); + testInjector.register("performanceService", { + now: () => 0, + processExecutionData: () => { + /* not measured here */ + }, + }); + testInjector.register("projectConfigService", { + getValue: (_key: string, defaultValue: any) => defaultValue, + }); + testInjector.register("projectData", { fromContainer: true }); + testInjector.register("hooksService", HooksService); + return testInjector; +} + +function writeHook( + projectDir: string, + hookName: string, + source: string, + extension = ".js", +): string { + const hooksDir = path.join(projectDir, "hooks"); + fs.mkdirSync(hooksDir, { recursive: true }); + const fullPath = path.join(hooksDir, `${hookName}${extension}`); + fs.writeFileSync(fullPath, source); + return fullPath; +} + +function writeHookInDirectory( + projectDir: string, + hookName: string, + fileName: string, + source: string, +): string { + const hooksDir = path.join(projectDir, "hooks", hookName); + fs.mkdirSync(hooksDir, { recursive: true }); + const fullPath = path.join(hooksDir, fileName); + fs.writeFileSync(fullPath, source); + return fullPath; +} + +describe("defineHook", () => { + let projectDir: string; + let testInjector: IInjector; + let capture: any; + + const hooksService = (): IHooksService => + testInjector.resolve("hooksService"); + const logger = (): LoggerStub => testInjector.resolve("logger"); + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-define-hook-")); + testInjector = createTestInjector(projectDir); + capture = (global).__hookCapture = {}; + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + delete (global).__hookCapture; + }); + + it("passes the hookArgs value as the payload, by identity and mutable in place", async () => { + writeHook( + projectDir, + "before-case1", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case1", async (ctx) => { + global.__hookCapture.payload = ctx.payload; + ctx.payload.args.push("--offline"); + });`, + ); + + const args = ["assembleDebug"]; + const payload = { args }; + await hooksService().executeBeforeHooks("case1", { hookArgs: payload }); + + assert.strictEqual(capture.payload, payload); + assert.deepEqual(args, ["assembleDebug", "--offline"]); + }); + + it("passes the top-level bag as the payload when there is no hookArgs wrapper", async () => { + writeHook( + projectDir, + "after-case2", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("after-case2", (ctx) => { + global.__hookCapture.payload = ctx.payload; + });`, + ); + + const liveSyncResultInfo = { fake: true }; + await hooksService().executeAfterHooks("case2", { liveSyncResultInfo }); + + assert.strictEqual(capture.payload.liveSyncResultInfo, liveSyncResultInfo); + }); + + it("leaves the payload undefined for a hook point with no arguments", async () => { + writeHook( + projectDir, + "before-case3", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case3", (ctx) => { + global.__hookCapture.ran = true; + global.__hookCapture.payload = ctx.payload; + });`, + ); + + await hooksService().executeBeforeHooks("case3"); + + assert.isTrue(capture.ran); + assert.isUndefined(capture.payload); + }); + + it("runs the handler in an injection context, so inject() resolves by token and by name", async () => { + writeHook( + projectDir, + "before-case4", + `const { defineHook, inject, Injector } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case4", async (ctx) => { + global.__hookCapture.container = inject(Injector); + global.__hookCapture.logger = inject("logger"); + });`, + ); + + await hooksService().executeBeforeHooks("case4"); + + assert.strictEqual(capture.container, testInjector); + assert.strictEqual(capture.logger, testInjector.resolve("logger")); + }); + + it("folds a wrap() middleware into the chain around the @hook-decorated method", async () => { + writeHook( + projectDir, + "before-case5", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case5", (ctx) => { + ctx.wrap(function (args, next) { + global.__hookCapture.middlewareArgs = args.slice(); + return next.apply(null, args).then(function (result) { + return "wrapped(" + result + ")"; + }); + }); + });`, + ); + + class Subject { + constructor(public $hooksService: IHooksService) {} + + @hook("case5") + async doWork(input: string): Promise { + (global).__hookCapture.originalRan = true; + return "original:" + input; + } + } + + const subject = testInjector.resolve(Subject); + const result = await subject.doWork("x"); + + assert.equal(result, "wrapped(original:x)"); + assert.isTrue(capture.originalRan); + assert.deepEqual(capture.middlewareArgs, ["x"]); + }); + + it("lets a wrap() middleware short-circuit the decorated method", async () => { + writeHook( + projectDir, + "before-case6", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case6", (ctx) => { + ctx.wrap(function () { + return "short-circuited"; + }); + });`, + ); + + class Subject { + constructor(public $hooksService: IHooksService) {} + + @hook("case6") + async doWork(): Promise { + (global).__hookCapture.originalRan = true; + return "original"; + } + } + + const subject = testInjector.resolve(Subject); + const result = await subject.doWork(); + + assert.equal(result, "short-circuited"); + assert.isUndefined(capture.originalRan); + }); + + it("warns and continues the command when the handler skips, stopping the handler", async () => { + writeHook( + projectDir, + "before-case7", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case7", async (ctx) => { + ctx.skip("soft-skip"); + global.__hookCapture.afterSkip = true; + });`, + ); + + await hooksService().executeBeforeHooks("case7"); + + assert.include(logger().warnOutput, "soft-skip"); + assert.isUndefined(capture.afterSkip); + }); + + it("fails the command when the handler fails, stopping the handler", async () => { + writeHook( + projectDir, + "before-case8", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case8", async (ctx) => { + ctx.fail("hard-fail"); + global.__hookCapture.afterFail = true; + });`, + ); + + await assert.isRejected( + hooksService().executeBeforeHooks("case8"), + /hard-fail/, + ); + assert.isUndefined(capture.afterFail); + }); + + it("keeps a legacy param-name hook on the old path, and never reports a definition hook", async () => { + const legacyPath = writeHookInDirectory( + projectDir, + "before-case9", + "legacy.js", + `module.exports = function ($logger) { + global.__hookCapture.legacyRan = true; + };`, + ); + const definitionPath = writeHookInDirectory( + projectDir, + "before-case9", + "modern.js", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case9", () => { + global.__hookCapture.definitionRan = true; + });`, + ); + + await hooksService().executeBeforeHooks("case9"); + + assert.isTrue(capture.legacyRan); + assert.isTrue(capture.definitionRan); + + const deprecationReports = logger() + .traceOutput.split("\n") + .filter((line) => line.indexOf("hooks.param-name-signature") !== -1); + assert.isTrue( + deprecationReports.some((line) => line.indexOf(legacyPath) !== -1), + ); + assert.isFalse( + deprecationReports.some((line) => line.indexOf(definitionPath) !== -1), + ); + }); + + it("recognizes a definition default-exported from an .mjs hook", async () => { + writeHook( + projectDir, + "before-case10", + `import { createRequire } from "module"; + const require = createRequire(import.meta.url); + const { defineHook } = require(${JSON.stringify(apiPath)}); + export default defineHook("before-case10", (ctx) => { + global.__hookCapture.payload = ctx.payload; + });`, + ".mjs", + ); + + const payload = { fromMjs: true }; + await hooksService().executeBeforeHooks("case10", { hookArgs: payload }); + + assert.strictEqual(capture.payload, payload); + }); + + it("skips a definition whose name differs from the hook point, with a warning", async () => { + writeHook( + projectDir, + "before-case11", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-something-else", () => { + global.__hookCapture.ran = true; + });`, + ); + + await hooksService().executeBeforeHooks("case11"); + + assert.isUndefined(capture.ran); + assert.include(logger().warnOutput, `defines the "before-something-else"`); + assert.include(logger().warnOutput, `"before-case11" hook point`); + }); + + it("accepts the object bag form", async () => { + writeHook( + projectDir, + "before-case12", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook({ + name: "before-case12", + run: (ctx) => { + global.__hookCapture.payload = ctx.payload; + }, + });`, + ); + + const payload = { fromBag: true }; + await hooksService().executeBeforeHooks("case12", { hookArgs: payload }); + + assert.strictEqual(capture.payload, payload); + }); + + it("rejects a wrap() at a hook point that consumes no middlewares", async () => { + writeHook( + projectDir, + "before-case13", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case13", (ctx) => { + ctx.wrap((args, next) => next(...args)); + });`, + ); + + await assert.isRejected( + hooksService().executeBeforeHooks("case13"), + /ctx\.wrap\(\) is not available at the "before-case13" hook point/, + ); + }); + + it("rejects a wrap() from an after-hook", async () => { + writeHook( + projectDir, + "after-case14", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("after-case14", (ctx) => { + ctx.wrap((args, next) => next(...args)); + });`, + ); + + await assert.isRejected( + hooksService().executeAfterHooks("case14"), + /ctx\.wrap\(\) is not available at the "after-case14" hook point/, + ); + }); + + it("defaults the fail() message instead of failing with Error(undefined)", async () => { + writeHook( + projectDir, + "before-case15", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case15", (ctx) => { + ctx.fail(); + });`, + ); + + await assert.isRejected( + hooksService().executeBeforeHooks("case15"), + /The "before-case15" hook called ctx\.fail\(\) without a message\./, + ); + }); + + it("defaults the skip() message", async () => { + writeHook( + projectDir, + "before-case18", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case18", (ctx) => { + ctx.skip(); + });`, + ); + + await hooksService().executeBeforeHooks("case18"); + + assert.include( + logger().warnOutput, + 'The "before-case18" hook called ctx.skip() without a message.', + ); + }); + + it("warns when a definition returns a function instead of calling ctx.wrap()", async () => { + writeHook( + projectDir, + "before-case16", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = defineHook("before-case16", () => { + return () => "legacy middleware"; + });`, + ); + + await hooksService().executeBeforeHooks("case16"); + + assert.include(logger().warnOutput, "returned a function"); + }); + + it("rejects an array export, naming the file", async () => { + const fullPath = writeHook( + projectDir, + "before-case17", + `const { defineHook } = require(${JSON.stringify(apiPath)}); + module.exports = [defineHook("before-case17", () => {})];`, + ); + + await assert.isRejected( + hooksService().executeBeforeHooks("case17"), + new RegExp( + `${fullPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} exports an array`, + ), + ); + }); +}); + +describe("defineHook validation", () => { + // The negative cases are exactly the ones the types reject, so they need an + // untyped view of the same function. + const defineHookUnsafe: any = defineHook; + + it("carries the payload generic through to ctx.payload", () => { + const definition = defineHook<{ args: string[] }>( + "before-build-task-args", + (ctx) => { + // Compile-time: `payload` is `{ args: string[] } | undefined`, so it + // needs narrowing before use. + assert.isUndefined(ctx.payload?.args); + }, + ); + + assert.isTrue(isHookDefinition(definition)); + }); + + it("rejects a bag with an unknown field, naming it and the accepted forms", () => { + assert.throws( + () => defineHookUnsafe({ name: "before-prepare", handler: () => {} }), + /unknown field "handler".*Supported fields: "name", "run".*Accepted forms/s, + ); + }); + + it("rejects a bag with no run", () => { + assert.throws( + () => defineHookUnsafe({ name: "before-prepare" }), + /"before-prepare".*requires "run" to be a function/, + ); + }); + + it("rejects a bag with no name", () => { + assert.throws( + () => defineHookUnsafe({ run: () => {} }), + /requires a non-empty "name"/, + ); + }); + + it("rejects the positional form without a handler function", () => { + assert.throws( + () => defineHookUnsafe("before-prepare"), + /"before-prepare".*requires a handler function as its second argument/, + ); + }); + + it("rejects a non-object, non-string argument", () => { + assert.throws( + () => defineHookUnsafe(undefined), + /called with an unsupported argument/, + ); + }); + + it("keeps the marker through a spread, so derived definitions stay recognizable", () => { + const definition = defineHook("before-prepare", () => {}); + const derived = { ...definition, name: "before-build" }; + + assert.isTrue(isHookDefinition(definition)); + assert.isTrue(isHookDefinition(derived)); + assert.equal(derived.name, "before-build"); + }); + + it("does not recognize a hand-rolled object", () => { + assert.isFalse(isHookDefinition({ name: "before-prepare", run: () => {} })); + }); +}); diff --git a/test/deprecation.ts b/test/deprecation.ts new file mode 100644 index 0000000000..8de2df0592 --- /dev/null +++ b/test/deprecation.ts @@ -0,0 +1,101 @@ +import { assert } from "chai"; +import { Yok, getInjector, setGlobalInjector } from "../lib/common/yok"; +import { + reportDeprecation, + clearReportedDeprecations, +} from "../lib/common/deprecation"; +import { LoggerStub } from "./stubs"; + +describe("deprecation tracer", () => { + let logger: LoggerStub; + let originalEnv: string | undefined; + + beforeEach(() => { + logger = new LoggerStub(); + clearReportedDeprecations(); + originalEnv = process.env.NS_DEPRECATIONS; + delete process.env.NS_DEPRECATIONS; + }); + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.NS_DEPRECATIONS; + } else { + process.env.NS_DEPRECATIONS = originalEnv; + } + }); + + it("logs at trace level by default and reports once per api+detail", () => { + reportDeprecation({ api: "test.api", detail: "site-1", logger }); + reportDeprecation({ api: "test.api", detail: "site-1", logger }); + + const occurrences = logger.traceOutput.split("test.api").length - 1; + assert.equal(occurrences, 1); + assert.equal(logger.warnOutput, ""); + + reportDeprecation({ api: "test.api", detail: "site-2", logger }); + assert.include(logger.traceOutput, "site-2"); + }); + + it("escalates to warn with NS_DEPRECATIONS=warn", () => { + process.env.NS_DEPRECATIONS = "warn"; + + reportDeprecation({ api: "test.warn", logger }); + + assert.include(logger.warnOutput, "test.warn"); + assert.equal(logger.traceOutput, ""); + }); + + it("throws with NS_DEPRECATIONS=error — on every call, not just the first", () => { + process.env.NS_DEPRECATIONS = "error"; + + assert.throws( + () => reportDeprecation({ api: "test.error", logger }), + /test\.error/, + ); + assert.throws( + () => reportDeprecation({ api: "test.error", logger }), + /test\.error/, + ); + }); + + it("falls back to the process-wide injector's logger when none is passed", () => { + const previousInjector = getInjector(); + const freshInjector = new Yok(); + const freshLogger = new LoggerStub(); + freshInjector.register("logger", freshLogger); + setGlobalInjector(freshInjector); + + try { + reportDeprecation({ api: "test.global-logger" }); + assert.include(freshLogger.traceOutput, "test.global-logger"); + } finally { + setGlobalInjector(previousInjector); + } + }); + + it("drops the report silently when no logger is resolvable", () => { + const previousInjector = getInjector(); + setGlobalInjector(new Yok()); + + try { + assert.doesNotThrow(() => reportDeprecation({ api: "test.no-logger" })); + } finally { + setGlobalInjector(previousInjector); + } + }); + + it("still delivers a report that was previously dropped for lack of a logger", () => { + const previousInjector = getInjector(); + setGlobalInjector(new Yok()); + try { + reportDeprecation({ api: "test.redeliver" }); + } finally { + setGlobalInjector(previousInjector); + } + + reportDeprecation({ api: "test.redeliver", logger }); + + assert.include(logger.traceOutput, "test.redeliver"); + }); +}); diff --git a/test/di.ts b/test/di.ts new file mode 100644 index 0000000000..27db548207 --- /dev/null +++ b/test/di.ts @@ -0,0 +1,650 @@ +import { assert } from "chai"; +import { + Injector, + inject, + runInInjectionContext, + Contract, + InjectionToken, + provide, + forwardRef, +} from "../lib/common/di"; + +@Contract({ name: "diTestGreeter" }) +abstract class Greeter { + abstract greet(): string; +} + +class GreeterImpl extends Greeter { + greet(): string { + return "hello"; + } +} + +@Contract({ name: "diTestDevice" }) +abstract class Device { + abstract id: string; +} + +describe("di: tokens and resolution", () => { + it("resolves one singleton via the class, the name, and the $-prefixed name", () => { + const injector = new Injector([provide(Greeter, GreeterImpl)]); + + const byClass = injector.get(Greeter); + assert.instanceOf(byClass, GreeterImpl); + assert.strictEqual(injector.get("diTestGreeter"), byClass); + assert.strictEqual(injector.get("$diTestGreeter"), byClass); + }); + + it("does not treat an implementation class as a token via its inherited contract name", () => { + const injector = new Injector([provide(Greeter, GreeterImpl)]); + + assert.throws(() => injector.get(GreeterImpl), /unable to resolve/); + }); + + it("throws on a duplicate contract name at declaration time", () => { + assert.throws(() => { + @Contract({ name: "diTestGreeter" }) + abstract class Duplicate {} + void Duplicate; + }, /already used/); + }); + + it("resolves a duplicated contract copy (same name, different class object) to the same provider", () => { + const injector = new Injector([provide(Greeter, GreeterImpl)]); + const original = injector.get(Greeter); + + // Simulates what a duplicated CLI copy's decorator does: same Symbol.for + // key, its own class object, its own registry. + abstract class DuplicatedCopy {} + Object.defineProperty( + DuplicatedCopy, + Symbol.for("nativescript:di:contractName"), + { value: "diTestGreeter" }, + ); + + assert.strictEqual(injector.get(DuplicatedCopy), original); + }); +}); + +describe("di: InjectionToken", () => { + // A module namespace object — the case that has no class to decorate. + const moduleValue = { parse: () => "parsed" }; + const MODULE_TOKEN = new InjectionToken( + "diTestModuleToken", + ); + + it("resolves one singleton via the token, the name, and the $-prefixed name", () => { + const injector = new Injector([ + { provide: MODULE_TOKEN, useValue: moduleValue }, + ]); + + assert.strictEqual(injector.get(MODULE_TOKEN), moduleValue); + assert.strictEqual(injector.get("diTestModuleToken"), moduleValue); + assert.strictEqual(injector.get("$diTestModuleToken"), moduleValue); + }); + + it("finds a registration made under the legacy name only", () => { + // The shape lib/node/xcode.ts registers today: a name, no token in sight. + const injector = new Injector([ + { provide: "diTestModuleToken", useValue: moduleValue }, + ]); + + assert.strictEqual(injector.get(MODULE_TOKEN), moduleValue); + }); + + it("resolves a factory-backed token to one instance across all spellings", () => { + let builds = 0; + const injector = new Injector([ + { + provide: MODULE_TOKEN, + useFactory: () => { + builds++; + return moduleValue; + }, + }, + ]); + + const first = injector.get(MODULE_TOKEN); + assert.strictEqual(injector.get("diTestModuleToken"), first); + assert.strictEqual(injector.get("$diTestModuleToken"), first); + assert.equal(builds, 1); + }); + + it("strips a leading $ from the description", () => { + const token = new InjectionToken("$diTestDollarToken"); + + assert.equal(token.description, "diTestDollarToken"); + const injector = new Injector([{ provide: token, useValue: moduleValue }]); + assert.strictEqual(injector.get("diTestDollarToken"), moduleValue); + }); + + it("resolves through a child scope, which can shadow it by name", () => { + const childValue = { parse: () => "child" }; + const root = new Injector([ + { provide: MODULE_TOKEN, useValue: moduleValue }, + ]); + const child = root.createChild(); + + assert.strictEqual(child.get(MODULE_TOKEN), moduleValue); + + const shadowing = root.createChild([ + { provide: "diTestModuleToken", useValue: childValue }, + ]); + assert.strictEqual(shadowing.get(MODULE_TOKEN), childValue); + assert.strictEqual(root.get(MODULE_TOKEN), moduleValue); + }); + + it("honours optional on a miss", () => { + const injector = new Injector(); + + assert.isNull(injector.get(MODULE_TOKEN, { optional: true })); + runInInjectionContext(injector, () => { + assert.isNull(inject(MODULE_TOKEN, { optional: true })); + }); + }); + + it("names the token in an unresolvable error", () => { + const injector = new Injector(); + + assert.throws( + () => injector.get(MODULE_TOKEN), + /unable to resolve InjectionToken\(diTestModuleToken\)/, + ); + }); + + it("names the token in a cyclic dependency report", () => { + const CYCLE_TOKEN = new InjectionToken("diTestCycleToken"); + class CycleConsumer { + constructor(public $diTestCycleToken: any) {} + } + const injector = new Injector([ + { provide: CYCLE_TOKEN, useLegacyClass: CycleConsumer }, + ]); + + assert.throws( + () => injector.get(CYCLE_TOKEN), + /InjectionToken\(diTestCycleToken\) -> InjectionToken\(diTestCycleToken\)/, + ); + }); + + it("throws on a duplicate description at construction time", () => { + assert.throws( + () => new InjectionToken("diTestModuleToken"), + /already used by an injection token/, + ); + }); + + it("throws when a description collides with a contract name", () => { + assert.throws( + () => new InjectionToken("diTestGreeter"), + /already used by contract 'Greeter'/, + ); + }); + + it("throws when a contract name collides with a token description", () => { + assert.throws(() => { + @Contract({ name: "diTestModuleToken" }) + abstract class Collides {} + void Collides; + }, /already used by an injection token/); + }); + + it("recognizes a token minted by a duplicated copy of the module", () => { + const injector = new Injector([ + { provide: MODULE_TOKEN, useValue: moduleValue }, + ]); + + // Same Symbol.for marker, its own object — what a second CLI copy mints. + const duplicatedCopy = {}; + Object.defineProperty( + duplicatedCopy, + Symbol.for("nativescript:di:injectionTokenName"), + { value: "diTestModuleToken" }, + ); + + assert.strictEqual(injector.get(duplicatedCopy), moduleValue); + }); +}); + +describe("di: lazy providers", () => { + it("does not invoke the loader until first get()", () => { + let loads = 0; + const injector = new Injector([ + { + provide: "lazyThing", + useLazyClass: () => { + loads++; + return GreeterImpl; + }, + }, + ]); + + assert.equal(loads, 0); + const first = injector.get("lazyThing"); + assert.equal(loads, 1); + assert.strictEqual(injector.get("lazyThing"), first); + assert.equal(loads, 1); + }); + + it("consumes a pending side-effect loader once, expecting it to register the resolver", () => { + let loads = 0; + const injector = new Injector(); + injector.register({ + provide: "lazyRequired", + useLazyRequire: () => { + loads++; + injector.register({ + provide: "lazyRequired", + useLegacyClass: class LazyRequiredThing {}, + }); + }, + }); + + const instance = injector.get("lazyRequired"); + assert.isOk(instance); + assert.equal(loads, 1); + assert.strictEqual(injector.get("lazyRequired"), instance); + assert.equal(loads, 1); + }); +}); + +describe("di: per-level lookup order", () => { + it("a child's string-keyed override shadows the parent for class-token consumers", () => { + const root = new Injector([ + { provide: Device, useValue: { id: "shared-singleton" } }, + ]); + const child = root.createChild([ + { provide: "diTestDevice", useValue: { id: "per-call" } }, + ]); + + // The class key misses in the child; the name fallback must hit the + // child's entry BEFORE lookup delegates to the parent — otherwise the + // per-call override is silently skipped. + assert.equal(child.get(Device).id, "per-call"); + assert.equal(child.get("diTestDevice").id, "per-call"); + assert.equal(root.get(Device).id, "shared-singleton"); + }); +}); + +describe("di: child scopes", () => { + it("hydrated children see the payload, fall back for services, and stay isolated", () => { + const loggerValue = { log: true }; + const payloadA = { args: ["a"] }; + const payloadB = { args: ["b"] }; + + const root = new Injector([{ provide: "logger", useValue: loggerValue }]); + const childA = root.createChild([ + { provide: "hookArgs", useValue: payloadA }, + ]); + const childB = root.createChild([ + { provide: "hookArgs", useValue: payloadB }, + ]); + + assert.strictEqual(childA.get("hookArgs"), payloadA); + assert.strictEqual(childA.get("$hookArgs"), payloadA); + assert.strictEqual(childB.get("hookArgs"), payloadB); + assert.strictEqual(childA.get("logger"), loggerValue); + assert.throws(() => root.get("hookArgs"), /unable to resolve/); + }); + + it("inject(Injector) returns the nearest injector", () => { + const root = new Injector(); + const child = root.createChild(); + + runInInjectionContext(child, () => { + assert.strictEqual(inject(Injector), child); + }); + runInInjectionContext(root, () => { + assert.strictEqual(inject(Injector), root); + }); + }); +}); + +describe("di: forwardRef", () => { + it("defers a token reference from provider-literal creation to registration", () => { + let LateToken: any; + // The literal is built while the binding is still unassigned — the thunk + // is only read when the injector processes the provider. + const providers = [ + { provide: forwardRef(() => LateToken), useClass: GreeterImpl }, + ]; + LateToken = Greeter; + + const injector = new Injector(providers); + + const instance = injector.get(Greeter); + assert.instanceOf(instance, GreeterImpl); + assert.strictEqual(injector.get("diTestGreeter"), instance); + }); + + it("resolves forwardRef tokens at lookup and inside inject()", () => { + const injector = new Injector([provide(Greeter, GreeterImpl)]); + const direct = injector.get(Greeter); + + assert.strictEqual(injector.get(forwardRef(() => Greeter)), direct); + runInInjectionContext(injector, () => { + assert.strictEqual(inject(forwardRef(() => Greeter)), direct); + }); + }); +}); + +describe("di: cross-copy injection context", () => { + // inject.js has no runtime imports, so a copied file loaded from another + // path is a genuine second instance of the module — the same situation as + // a nested nativescript install serving a hook or extension module. + const loadSecondCopy = (): any => { + const fs = require("fs"); + const os = require("os"); + const path = require("path"); + const source = require.resolve("../lib/common/di/inject.js"); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-di-copy-")); + const target = path.join(dir, "inject.js"); + fs.copyFileSync(source, target); + return require(target); + }; + + it("a second copy's inject() resolves against the running copy's context, with a one-time warning", () => { + const copyB = loadSecondCopy(); + const warnings: string[] = []; + const loggerValue = { + warn: (message: string) => warnings.push(message), + }; + const injector = new Injector([ + { provide: "logger", useValue: loggerValue }, + ]); + + runInInjectionContext(injector, () => { + // The running copy serving its own context never warns. + assert.strictEqual(inject("logger"), loggerValue); + assert.equal(warnings.length, 0); + + // The second copy resolves through the shared slot — and warns once. + assert.strictEqual(copyB.inject("logger"), loggerValue); + assert.strictEqual(copyB.inject("logger"), loggerValue); + }); + + assert.equal(warnings.length, 1); + assert.include(warnings[0], "second copy of the NativeScript CLI"); + assert.include(warnings[0], "peerDependency"); + }); + + it("a second copy outside any context still throws the teaching error", () => { + const copyB = loadSecondCopy(); + assert.throws(() => copyB.inject("logger"), /injection context/); + }); +}); + +describe("di: inject options", () => { + it("optional resolves to null for an unknown token, and normally for a known one", () => { + const injector = new Injector([provide(Greeter, GreeterImpl)]); + + assert.isNull(injector.get("nothing-here", { optional: true })); + assert.instanceOf(injector.get(Greeter, { optional: true }), GreeterImpl); + + runInInjectionContext(injector, () => { + assert.isNull(inject("nothing-here", { optional: true })); + }); + }); + + it("optional does not swallow a found-but-misconfigured record", () => { + const injector = new Injector([ + { provide: "brokenLiteral", useValue: {}, shared: false }, + ]); + + assert.throws( + () => injector.get("brokenLiteral", { optional: true }), + /no resolver registered/, + ); + }); + + it("skipSelf escapes a child scope's shadowing entry", () => { + const root = new Injector([ + { provide: "logger", useValue: { from: "root" } }, + ]); + const child = root.createChild([ + { provide: "logger", useValue: { from: "payload" } }, + ]); + + assert.equal(child.get("logger").from, "payload"); + assert.equal(child.get("logger", { skipSelf: true }).from, "root"); + // On the root there is no parent to skip to. + assert.isNull(root.get("logger", { skipSelf: true, optional: true })); + }); + + it("self refuses parent fallthrough", () => { + const root = new Injector([{ provide: "rootOnly", useValue: { tag: 1 } }]); + const child = root.createChild([ + { provide: "childOnly", useValue: { tag: 2 } }, + ]); + + assert.equal(child.get("childOnly", { self: true }).tag, 2); + assert.throws( + () => child.get("rootOnly", { self: true }), + /unable to resolve/, + ); + assert.isNull(child.get("rootOnly", { self: true, optional: true })); + }); + + it("rejects combining self and skipSelf", () => { + const injector = new Injector(); + assert.throws( + () => injector.get("anything", { self: true, skipSelf: true }), + /cannot combine self and skipSelf/, + ); + }); +}); + +describe("di: cycles", () => { + it("reports the full resolution path", () => { + class CycleA { + constructor(public $cycleB: any) {} + } + class CycleB { + constructor(public $cycleA: any) {} + } + const injector = new Injector([ + { provide: "cycleA", useLegacyClass: CycleA }, + { provide: "cycleB", useLegacyClass: CycleB }, + ]); + + assert.throws( + () => injector.get("cycleA"), + /Cyclic dependency detected on dependency 'cycleA'.*cycleA -> cycleB -> cycleA/, + ); + }); +}); + +describe("di: transients and disposal", () => { + it("shared:false constructs per resolution, retains every instance, and disposes in reverse order", () => { + const disposed: number[] = []; + let seq = 0; + const injector = new Injector([ + { + provide: "transientThing", + shared: false, + useFactory: () => { + const id = ++seq; + return { + id, + dispose: () => disposed.push(id), + }; + }, + }, + ]); + + const first = injector.get("transientThing"); + const second = injector.get("transientThing"); + assert.notStrictEqual(first, second); + + injector.dispose(); + assert.deepEqual(disposed, [2, 1]); + }); + + it("disposes shared singletons in reverse instantiation order", () => { + const disposed: string[] = []; + const injector = new Injector([ + { + provide: "firstService", + useFactory: () => ({ dispose: () => disposed.push("first") }), + }, + { + provide: "secondService", + useFactory: () => ({ dispose: () => disposed.push("second") }), + }, + ]); + + injector.get("firstService"); + injector.get("secondService"); + injector.dispose(); + + assert.deepEqual(disposed, ["second", "first"]); + }); +}); + +describe("di: createInstance", () => { + class MidDep { + constructor(public $leafDep: any) {} + } + + class EntryConsumer { + constructor( + public $midDep: any, + public $leafDep: any, + ) {} + } + + it("resolves annotated $-params, with per-call providers shadowing one level only", () => { + const root = new Injector([ + { provide: "leafDep", useValue: { tag: "root-leaf" } }, + { provide: "midDep", useLegacyClass: MidDep }, + ]); + + const instance = root.createInstance(EntryConsumer, [ + { provide: "leafDep", useValue: { tag: "override-leaf" } }, + ]); + + assert.equal(instance.$leafDep.tag, "override-leaf"); + // The nested dependency is constructed by its owning injector, so the + // per-call override must not leak into it — Yok's bag never propagated. + assert.equal(instance.$midDep.$leafDep.tag, "root-leaf"); + }); + + it("applies a raw ctorArguments bag with own-key semantics and no $ normalization", () => { + const root = new Injector([ + { provide: "leafDep", useValue: { tag: "root-leaf" } }, + { provide: "midDep", useLegacyClass: MidDep }, + ]); + const fakeMid = { fake: "mid" }; + + const instance = root.createInstance(EntryConsumer, [], { + $midDep: fakeMid, + }); + + assert.strictEqual(instance.$midDep, fakeMid); + assert.equal(instance.$leafDep.tag, "root-leaf"); + }); + + it("invokes lowercase resolvers as factories instead of new-ing them", () => { + const injector = new Injector([ + { + provide: "factoryMade", + useLegacyClass: function makeThing() { + return { viaFactory: true }; + }, + }, + ]); + + assert.isTrue(injector.get("factoryMade").viaFactory); + }); +}); + +describe("di: inject()", () => { + it("works in field initializers during construction", () => { + class UsesInject { + public greeter = inject(Greeter); + public injector = inject(Injector); + } + const injector = new Injector([ + provide(Greeter, GreeterImpl), + { provide: "usesInject", useClass: UsesInject }, + ]); + + const instance = injector.get("usesInject"); + assert.instanceOf(instance.greeter, GreeterImpl); + assert.strictEqual(instance.injector, injector); + }); + + it("throws outside an injection context", () => { + assert.throws(() => inject(Greeter), /injection context/); + }); + + it("restores the previous context, including across nesting", () => { + const outer = new Injector(); + const inner = new Injector(); + + runInInjectionContext(outer, () => { + runInInjectionContext(inner, () => { + assert.strictEqual(inject(Injector), inner); + }); + assert.strictEqual(inject(Injector), outer); + }); + assert.throws(() => inject(Injector), /injection context/); + }); +}); + +describe("di: register semantics", () => { + it("a non-shared object literal has no resolver — Yok quirk preserved", () => { + const injector = new Injector([ + { provide: "literalTransient", useValue: {}, shared: false }, + ]); + + assert.throws( + () => injector.get("literalTransient"), + /no resolver registered/, + ); + }); + + it("re-registering a shared value replaces the cached instance", () => { + const injector = new Injector([{ provide: "config", useValue: { v: 1 } }]); + assert.equal(injector.get("config").v, 1); + + injector.register({ provide: "config", useValue: { v: 2 } }); + assert.equal(injector.get("config").v, 2); + }); + + it("re-registering a resolver keeps an already-cached instance", () => { + const injector = new Injector([ + { provide: "svc", useFactory: () => ({ v: 1 }) }, + ]); + const first = injector.get("svc"); + + injector.register({ provide: "svc", useFactory: () => ({ v: 2 }) }); + assert.strictEqual(injector.get("svc"), first); + }); + + it("a contract registration joins an existing record under the same name", () => { + const injector = new Injector([ + { provide: "diTestGreeter", useValue: { preexisting: true } }, + ]); + const cached = injector.get("diTestGreeter"); + + injector.register(provide(Greeter, GreeterImpl)); + + // Mutate-not-replace: the class key now aliases the same record, whose + // cached instance wins over the newly registered resolver. + assert.strictEqual(injector.get(Greeter), cached); + }); + + it("enumerates registered names by prefix", () => { + const injector = new Injector([ + { provide: "commands.build", useValue: {} }, + { provide: "commands.run", useValue: {} }, + { provide: "unrelated", useValue: {} }, + ]); + + assert.sameMembers(injector.getRegisteredNames("commands."), [ + "commands.build", + "commands.run", + ]); + }); +}); diff --git a/test/extension-manifests.ts b/test/extension-manifests.ts new file mode 100644 index 0000000000..de902a81e4 --- /dev/null +++ b/test/extension-manifests.ts @@ -0,0 +1,973 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { ExtensibilityService } from "../lib/services/extensibility-service"; +import { Yok, getInjector, setGlobalInjector } from "../lib/common/yok"; +import { LoggerStub } from "./stubs"; +import { clearReportedDeprecations } from "../lib/common/deprecation"; +import { CommandsDelimiters } from "../lib/common/constants"; +import { IInjector } from "../lib/common/definitions/yok"; +import { + IExtensibilityService, + IExtensionData, +} from "../lib/common/definitions/extensibility"; +import { IStringDictionary } from "../lib/common/declarations"; + +// Every assertion about registered commands goes through the per-test +// injector: the service takes $injector as a constructor dependency. The +// process-wide injector is pointed at that same instance for each test's +// duration ONLY because legacy-shape fixture modules register through the +// published global surface when they load - that swap is the legacy-compat +// seam, not the assertion path. Command names stay unique per test since the +// module require cache outlives a test. + +interface ITestCapture { + loadedModules: string[]; + executed: any[]; +} + +const DEPRECATION_API = "extensions.require-time-registration"; + +describe("extension manifests", () => { + let profileDir: string; + let requiredPaths: string[]; + let capture: ITestCapture; + let testInjector: IInjector; + let previousProcessInjector: IInjector; + + beforeEach(() => { + profileDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-ext-manifest-")); + testInjector = getTestInjector(); + previousProcessInjector = getInjector(); + setGlobalInjector(testInjector); + requiredPaths = []; + capture = (global).__nsmCapture = { + loadedModules: [], + executed: [], + }; + fs.mkdirSync(path.join(profileDir, "extensions", "node_modules"), { + recursive: true, + }); + writeExtensionsPackageJson({}); + clearReportedDeprecations(); + }); + + afterEach(() => { + setGlobalInjector(previousProcessInjector); + fs.rmSync(profileDir, { recursive: true, force: true }); + delete (global).__nsmCapture; + }); + + const writeExtensionsPackageJson = ( + dependencies: IStringDictionary, + ): void => { + fs.writeFileSync( + path.join(profileDir, "extensions", "package.json"), + JSON.stringify({ + name: "nativescript-extensibility", + version: "1.0.0", + dependencies, + }), + ); + }; + + /** + * Lays out a real extension package: package.json with the given nativescript + * key plus the given files, and an entry in the extensions dir dependencies. + */ + const writeExtension = ( + extensionName: string, + nativescript: any, + files: IStringDictionary, + ): string => { + const pathToExtension = path.join( + profileDir, + "extensions", + "node_modules", + extensionName, + ); + fs.mkdirSync(pathToExtension, { recursive: true }); + fs.writeFileSync( + path.join(pathToExtension, "package.json"), + JSON.stringify({ + name: extensionName, + version: "1.0.0", + main: "main.js", + nativescript, + }), + ); + + for (const relativePath of Object.keys(files || {})) { + const pathToFile = path.join(pathToExtension, relativePath); + fs.mkdirSync(path.dirname(pathToFile), { recursive: true }); + fs.writeFileSync(pathToFile, files[relativePath]); + } + + const pathToExtensionsPackageJson = path.join( + profileDir, + "extensions", + "package.json", + ); + const packageJsonData = JSON.parse( + fs.readFileSync(pathToExtensionsPackageJson).toString(), + ); + packageJsonData.dependencies[extensionName] = "1.0.0"; + fs.writeFileSync( + pathToExtensionsPackageJson, + JSON.stringify(packageJsonData), + ); + + return pathToExtension; + }; + + const mainModule = (marker: string): string => + `global.__nsmCapture.loadedModules.push(${JSON.stringify(marker)});`; + + const commandModule = (commandName: string, marker: string): string => + `class TestCommand { + constructor() { + this.allowedParameters = []; + } + async execute(args) { + global.__nsmCapture.executed.push({ marker: ${JSON.stringify( + marker, + )}, args: args }); + } + } + global.__nsmCapture.loadedModules.push(${JSON.stringify(marker)}); + global.$injector.registerCommand(${JSON.stringify(commandName)}, TestCommand);`; + + const getTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("fs", { + exists: (pathToCheck: string): boolean => fs.existsSync(pathToCheck), + readJson: (pathToFile: string): any => + JSON.parse(fs.readFileSync(pathToFile).toString()), + readText: (pathToFile: string): string => + fs.readFileSync(pathToFile).toString(), + readDirectory: (dir: string): string[] => fs.readdirSync(dir), + createDirectory: (dir: string): void => { + fs.mkdirSync(dir, { recursive: true }); + }, + writeJson: (pathToFile: string, content: any): void => + fs.writeFileSync(pathToFile, JSON.stringify(content)), + }); + testInjector.register("logger", LoggerStub); + testInjector.register("packageManager", { + install: async (): Promise => { + throw new Error("Extensions are expected to be installed already."); + }, + uninstall: async (): Promise => undefined, + searchNpms: async (): Promise => ({ results: [] }), + getRegistryPackageData: async (): Promise => ({}), + }); + testInjector.register("settingsService", { + getProfileDir: (): string => profileDir, + }); + testInjector.register("requireService", { + require: (module: string): any => { + requiredPaths.push(module); + return require(module); + }, + }); + + return testInjector; + }; + + const resolveService = (testInjector: IInjector): IExtensibilityService => + testInjector.resolve(ExtensibilityService); + + const getLogger = (testInjector: IInjector): LoggerStub => + testInjector.resolve("logger"); + + describe("commands declared as a map", () => { + it("registers each command lazily and never loads the extension main", async () => { + const extensionName = "nsm-lazy-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmlazy|run": "./dist/commands/run.js", + "nsmlazy|clean": "./dist/commands/clean.js", + }, + }, + { + "main.js": mainModule("lazy-main"), + "dist/commands/run.js": commandModule("nsmlazy|run", "lazy-run"), + "dist/commands/clean.js": commandModule( + "nsmlazy|clean", + "lazy-clean", + ), + }, + ); + + const extensibilityService = resolveService(testInjector); + const extensionData = + await extensibilityService.loadExtension(extensionName); + + assert.deepStrictEqual(capture.loadedModules, []); + assert.deepStrictEqual( + requiredPaths, + [], + "The extension main must not be required when its commands are declared as a map.", + ); + assert.notInclude(getLogger(testInjector).traceOutput, DEPRECATION_API); + assert.deepStrictEqual(extensionData.commands, [ + "nsmlazy|run", + "nsmlazy|clean", + ]); + + const command = testInjector.resolveCommand("nsmlazy|run"); + assert.isOk(command); + assert.deepStrictEqual(capture.loadedModules, ["lazy-run"]); + + await command.execute(["arg"]); + assert.deepStrictEqual(capture.executed, [ + { marker: "lazy-run", args: ["arg"] }, + ]); + + // The other command's module is still not loaded, and neither is main. + assert.deepStrictEqual(capture.loadedModules, ["lazy-run"]); + assert.deepStrictEqual(requiredPaths, []); + }); + + it("warns about and skips malformed entries, keeping the valid ones", async () => { + const extensionName = "nsm-malformed-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmbad|good": "./good.js", + "nsmbad|number": 42, + "nsmbad|empty": " ", + "": "./unnamed.js", + }, + }, + { + "main.js": mainModule("malformed-main"), + "good.js": commandModule("nsmbad|good", "malformed-good"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "nsmbad|number"); + assert.include(warnOutput, "nsmbad|empty"); + assert.include(warnOutput, extensionName); + + assert.isOk(testInjector.resolveCommand("nsmbad|good")); + assert.isNull(testInjector.resolveCommand("nsmbad|number")); + assert.isNull(testInjector.resolveCommand("nsmbad|empty")); + assert.deepStrictEqual(capture.loadedModules, ["malformed-good"]); + }); + + it("warns instead of failing when two extensions claim the same command", async () => { + const firstExtension = "nsm-first-ext"; + const secondExtension = "nsm-second-ext"; + writeExtension( + firstExtension, + { commands: { "nsmconflict|run": "./run.js" } }, + { + "main.js": mainModule("first-main"), + "run.js": commandModule("nsmconflict|run", "first-run"), + }, + ); + writeExtension( + secondExtension, + { commands: { "nsmconflict|run": "./run.js" } }, + { + "main.js": mainModule("second-main"), + "run.js": commandModule("nsmconflict|run", "second-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(firstExtension); + await extensibilityService.loadExtension(secondExtension); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "nsmconflict|run"); + assert.include(warnOutput, firstExtension); + assert.include(warnOutput, secondExtension); + + const command = testInjector.resolveCommand("nsmconflict|run"); + await command.execute([]); + assert.deepStrictEqual(capture.executed, [ + { marker: "first-run", args: [] }, + ]); + }); + }); + + describe("commands declared as an array", () => { + it("requires the extension main eagerly and reports the deprecated registration", async () => { + const extensionName = "nsm-eager-ext"; + const pathToExtension = writeExtension( + extensionName, + { commands: ["nsmeager|run"] }, + { + "main.js": `${mainModule("eager-main")} + ${commandModule("nsmeager|run", "eager-run")}`, + }, + ); + + const extensibilityService = resolveService(testInjector); + const extensionData = + await extensibilityService.loadExtension(extensionName); + + assert.deepStrictEqual(requiredPaths, [pathToExtension]); + assert.deepStrictEqual(capture.loadedModules, [ + "eager-main", + "eager-run", + ]); + + const traceOutput = getLogger(testInjector).traceOutput; + assert.include(traceOutput, DEPRECATION_API); + assert.include(traceOutput, extensionName); + + assert.deepStrictEqual(extensionData.commands, ["nsmeager|run"]); + assert.isOk(testInjector.resolveCommand("nsmeager|run")); + }); + + it("keeps the eager path when the extension declares no commands", async () => { + const extensionName = "nsm-no-commands-ext"; + const pathToExtension = writeExtension( + extensionName, + { docs: "./docs" }, + { "main.js": mainModule("no-commands-main") }, + ); + + const extensibilityService = resolveService(testInjector); + const extensionData = + await extensibilityService.loadExtension(extensionName); + + assert.deepStrictEqual(requiredPaths, [pathToExtension]); + assert.include(getLogger(testInjector).traceOutput, DEPRECATION_API); + assert.isUndefined(extensionData.commands); + }); + }); + + describe("getInstalledExtensionsData", () => { + it("reports the declared command names for both manifest shapes", () => { + writeExtension( + "nsm-data-map-ext", + { commands: { "nsmdata|one": "./one.js", "nsmdata|two": "./two.js" } }, + {}, + ); + writeExtension("nsm-data-array-ext", { commands: ["nsmdata|three"] }, {}); + writeExtension("nsm-data-plain-ext", {}, {}); + + const extensibilityService = resolveService(testInjector); + const extensionsData = extensibilityService.getInstalledExtensionsData(); + const dataByName: { [name: string]: IExtensionData } = {}; + for (const extensionData of extensionsData) { + dataByName[extensionData.extensionName] = extensionData; + } + + assert.deepStrictEqual(dataByName["nsm-data-map-ext"].commands, [ + "nsmdata|one", + "nsmdata|two", + ]); + assert.deepStrictEqual(dataByName["nsm-data-array-ext"].commands, [ + "nsmdata|three", + ]); + assert.isUndefined(dataByName["nsm-data-plain-ext"].commands); + }); + }); + + describe("getExtensionNameWhereCommandIsRegistered", () => { + const getExtensionCommandInfo = async ( + registryCommands: any, + inputStrings: string[], + ): Promise => { + const extensionName = "nsm-registry-ext"; + const packageManager = testInjector.resolve("packageManager"); + packageManager.searchNpms = async (keyword: string): Promise => { + assert.equal(keyword, "nativescript:extension"); + return { results: [{ package: { name: extensionName } }] }; + }; + packageManager.getRegistryPackageData = async (): Promise => ({ + ["dist-tags"]: { latest: "1.0.0" }, + versions: { + "1.0.0": { nativescript: { commands: registryCommands } }, + }, + }); + + const extensibilityService = resolveService(testInjector); + return extensibilityService.getExtensionNameWhereCommandIsRegistered({ + inputStrings, + commandDelimiter: CommandsDelimiters.HierarchicalCommand, + defaultCommandDelimiter: CommandsDelimiters.DefaultHierarchicalCommand, + }); + }; + + it("suggests an extension whose registry data declares commands as a map", async () => { + const result = await getExtensionCommandInfo( + { "registry|command": "./registry-command.js" }, + ["registry", "command", "and", "args"], + ); + + assert.deepStrictEqual(result, { + extensionName: "nsm-registry-ext", + registeredCommandName: "registry|command", + installationMessage: + "The command registry command is registered in extension nsm-registry-ext. You can install it by executing 'ns extension install nsm-registry-ext'", + }); + }); + + it("synthesizes the short form of a default command declared as a map", async () => { + const result = await getExtensionCommandInfo( + { + "registry|*default": "./registry-default.js", + "registry|other": "./registry-other.js", + }, + ["registry", "and", "args"], + ); + + assert.deepStrictEqual(result, { + extensionName: "nsm-registry-ext", + registeredCommandName: "registry", + installationMessage: + "The command registry is registered in extension nsm-registry-ext. You can install it by executing 'ns extension install nsm-registry-ext'", + }); + }); + + it("still suggests an extension whose registry data declares commands as an array", async () => { + const result = await getExtensionCommandInfo( + ["registry|*default", "registry|other"], + ["registry", "and", "args"], + ); + + assert.deepStrictEqual(result, { + extensionName: "nsm-registry-ext", + registeredCommandName: "registry", + installationMessage: + "The command registry is registered in extension nsm-registry-ext. You can install it by executing 'ns extension install nsm-registry-ext'", + }); + }); + + it("returns null when the declared commands do not match the input", async () => { + const result = await getExtensionCommandInfo( + { "registry|command": "./registry-command.js" }, + ["some", "other", "command"], + ); + + assert.isNull(result); + }); + }); + + describe("commands declared as defineCommand modules", () => { + const contractsPath = require.resolve("../lib/contracts"); + + const definitionModule = ( + commandName: string, + marker: string, + exportAs: string = "module.exports", + ): string => + `const { defineCommand } = require(${JSON.stringify(contractsPath)}); + global.__nsmCapture.loadedModules.push(${JSON.stringify(marker)}); + ${exportAs} = defineCommand({ + name: ${JSON.stringify(commandName)}, + arguments: "any", + async run(ctx) { + global.__nsmCapture.executed.push({ marker: ${JSON.stringify( + marker, + )}, args: ctx.args }); + }, + });`; + + it("adapts and registers a pure definition module lazily", async () => { + const extensionName = "nsm-def-ext"; + writeExtension( + extensionName, + { commands: { "nsmdef|hello": "./dist/hello.js" } }, + { + "main.js": mainModule("def-main"), + "dist/hello.js": definitionModule("nsmdef|hello", "def-hello"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.deepEqual(capture.loadedModules, []); + + const command = testInjector.resolveCommand("nsmdef|hello"); + assert.isOk(command); + assert.deepEqual(capture.loadedModules, ["def-hello"]); + + await command.execute(["fast"]); + assert.deepEqual(capture.executed, [ + { marker: "def-hello", args: ["fast"] }, + ]); + }); + + it("resolves the hierarchical parent dispatcher without loading any child module", async () => { + const extensionName = "nsm-defp-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmdefp|go": "./dist/go.js", + "nsmdefp|stop": "./dist/stop.js", + }, + }, + { + "main.js": mainModule("defp-main"), + "dist/go.js": definitionModule("nsmdefp|go", "defp-go"), + "dist/stop.js": definitionModule("nsmdefp|stop", "defp-stop"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const parent = testInjector.resolveCommand("nsmdefp"); + assert.isOk(parent); + assert.isTrue(parent.isHierarchicalCommand); + assert.deepEqual(capture.loadedModules, []); + assert.deepEqual(testInjector.getChildrenCommandsNames("nsmdefp"), [ + "go", + "stop", + ]); + + assert.isOk(testInjector.resolveCommand("nsmdefp|go")); + assert.deepEqual(capture.loadedModules, ["defp-go"]); + }); + + it("registers a definition under the manifest key and warns about a disagreeing name", async () => { + const extensionName = "nsm-defname-ext"; + writeExtension( + extensionName, + { commands: { "nsmdefname|run": "./dist/run.js" } }, + { + "main.js": mainModule("defname-main"), + "dist/run.js": definitionModule("something|else", "defname-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const command = testInjector.resolveCommand("nsmdefname|run"); + assert.isOk(command); + await command.execute([]); + assert.deepEqual(capture.executed, [{ marker: "defname-run", args: [] }]); + assert.isNull(testInjector.resolveCommand("something|else")); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "nsmdefname|run"); + assert.include(warnOutput, "something|else"); + assert.include(warnOutput, extensionName); + }); + + it("adapts a definition exported as the module's default", async () => { + const extensionName = "nsm-defdefault-ext"; + writeExtension( + extensionName, + { commands: { "nsmdefdefault|hello": "./dist/hello.js" } }, + { + "main.js": mainModule("defdefault-main"), + "dist/hello.js": definitionModule( + "nsmdefdefault|hello", + "defdefault-hello", + "exports.default", + ), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const command = testInjector.resolveCommand("nsmdefdefault|hello"); + assert.isOk(command); + await command.execute([]); + assert.deepEqual(capture.executed, [ + { marker: "defdefault-hello", args: [] }, + ]); + }); + + it("routes two aliases of one command to the same module", async () => { + const extensionName = "nsm-alias-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmalias|run": "./dist/run.js", + "nsmalias|r": "./dist/run.js", + }, + }, + { + "main.js": mainModule("alias-main"), + "dist/run.js": definitionModule("nsmalias|run", "alias-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.isOk(testInjector.resolveCommand("nsmalias|run")); + const aliased = testInjector.resolveCommand("nsmalias|r"); + assert.isOk(aliased); + + await aliased.execute(["x"]); + assert.deepEqual(capture.executed, [ + { marker: "alias-run", args: ["x"] }, + ]); + }); + }); + + describe("manifest entry values", () => { + it("accepts an object entry carrying the module path and ignores its other keys", async () => { + const extensionName = "nsm-envelope-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmenvelope|run": { + path: "./run.js", + somethingAddedLater: true, + }, + }, + }, + { + "main.js": mainModule("envelope-main"), + "run.js": commandModule("nsmenvelope|run", "envelope-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.deepEqual(capture.loadedModules, []); + assert.isOk(testInjector.resolveCommand("nsmenvelope|run")); + assert.deepEqual(capture.loadedModules, ["envelope-run"]); + }); + + it("warns about and skips an object entry without a usable path", async () => { + const extensionName = "nsm-envelope-bad-ext"; + writeExtension( + extensionName, + { commands: { "nsmenvelopebad|run": { module: "./run.js" } } }, + { "main.js": mainModule("envelope-bad-main") }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.include(getLogger(testInjector).warnOutput, "nsmenvelopebad|run"); + assert.isNull(testInjector.resolveCommand("nsmenvelopebad|run")); + }); + }); + + describe("manifest keys the CLI cannot route", () => { + it("rejects a key that is not lower case", async () => { + const extensionName = "nsm-case-ext"; + writeExtension( + extensionName, + { commands: { "nsmCase|Run": "./run.js" } }, + { + "main.js": mainModule("case-main"), + "run.js": commandModule("nsmcase|run", "case-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "nsmCase|Run"); + assert.include(warnOutput, "nsmcase|run"); + assert.include(warnOutput, extensionName); + assert.isNull(testInjector.resolveCommand("nsmCase|Run")); + assert.isNull(testInjector.resolveCommand("nsmcase|run")); + }); + + it("rejects a key already in use as the parent of its own subcommands", async () => { + const extensionName = "nsm-parentclash-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmparentclash|run": "./run.js", + nsmparentclash: "./flat.js", + }, + }, + { + "main.js": mainModule("parentclash-main"), + "run.js": commandModule("nsmparentclash|run", "parentclash-run"), + "flat.js": commandModule("nsmparentclash", "parentclash-flat"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "nsmparentclash"); + assert.include(warnOutput, "parent of its subcommands"); + + const parent = testInjector.resolveCommand("nsmparentclash"); + assert.isTrue(parent.isHierarchicalCommand); + assert.deepEqual(capture.loadedModules, []); + }); + + it("rejects a subcommand whose parent is a command of its own", async () => { + const extensionName = "nsm-parentcmd-ext"; + writeExtension( + extensionName, + { + commands: { + nsmparentcmd: "./flat.js", + "nsmparentcmd|run": "./run.js", + }, + }, + { + "main.js": mainModule("parentcmd-main"), + "flat.js": commandModule("nsmparentcmd", "parentcmd-flat"), + "run.js": commandModule("nsmparentcmd|run", "parentcmd-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, extensionName); + assert.include(warnOutput, "nsmparentcmd|run"); + assert.include(warnOutput, "already registered as a command of its own"); + assert.notInclude(warnOutput, "no subcommand dispatcher was created"); + + assert.isNull(testInjector.resolveCommand("nsmparentcmd|run")); + assert.isUndefined( + testInjector.getChildrenCommandsNames("nsmparentcmd"), + "The rejected subcommand must not be recorded under its parent.", + ); + + const command = testInjector.resolveCommand("nsmparentcmd"); + assert.isNotOk(command.isHierarchicalCommand); + await command.execute([]); + assert.deepEqual(capture.executed, [ + { marker: "parentcmd-flat", args: [] }, + ]); + + // Ownership of a rejected name is not recorded, so the same extension + // is told again rather than treated as the owner on the next load. + await extensibilityService.loadExtension(extensionName); + assert.equal( + getLogger(testInjector).warnOutput.split("nsmparentcmd|run").length - 1, + 2, + ); + }); + + it("reports a command the CLI itself provides without naming internals", async () => { + const extensionName = "nsm-builtin-ext"; + class BuiltInCommand { + public allowedParameters: any[] = []; + public async execute(): Promise { + return undefined; + } + } + testInjector.registerCommand("nsmbuiltin", BuiltInCommand); + + writeExtension( + extensionName, + { commands: { nsmbuiltin: "./run.js" } }, + { + "main.js": mainModule("builtin-main"), + "run.js": commandModule("nsmbuiltin", "builtin-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + const warnOutput = getLogger(testInjector).warnOutput; + assert.include(warnOutput, "already provided by the CLI"); + assert.include(warnOutput, extensionName); + assert.notInclude(warnOutput, "commands."); + + assert.instanceOf( + testInjector.resolveCommand("nsmbuiltin"), + BuiltInCommand, + ); + assert.deepEqual(capture.loadedModules, []); + }); + }); + + describe("manifest keys that name an Object prototype member", () => { + it("registers a flat command named after a prototype member", async () => { + const extensionName = "nsm-proto-flat-ext"; + writeExtension( + extensionName, + { commands: { constructor: "./run.js" } }, + { + "main.js": mainModule("proto-flat-main"), + "run.js": commandModule("constructor", "proto-flat-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.equal(getLogger(testInjector).warnOutput, ""); + + const command = testInjector.resolveCommand("constructor"); + assert.isOk(command); + await command.execute([]); + assert.deepEqual(capture.executed, [ + { marker: "proto-flat-run", args: [] }, + ]); + }); + + it("routes a subcommand whose parent names a prototype member", async () => { + const extensionName = "nsm-proto-parent-ext"; + writeExtension( + extensionName, + { commands: { "constructor|run": "./run.js" } }, + { + "main.js": mainModule("proto-parent-main"), + "run.js": commandModule("constructor|run", "proto-parent-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.equal(getLogger(testInjector).warnOutput, ""); + assert.deepEqual(testInjector.getChildrenCommandsNames("constructor"), [ + "run", + ]); + + const parent = testInjector.resolveCommand("constructor"); + assert.isTrue(parent.isHierarchicalCommand); + assert.deepEqual(capture.loadedModules, []); + + const command = testInjector.resolveCommand("constructor|run"); + assert.isOk(command); + await command.execute([]); + assert.deepEqual(capture.executed, [ + { marker: "proto-parent-run", args: [] }, + ]); + }); + }); + + describe("loading an already loaded extension", () => { + it("does not report the extension as conflicting with itself", async () => { + const extensionName = "nsm-reload-ext"; + writeExtension( + extensionName, + { commands: { "nsmreload|run": "./run.js" } }, + { + "main.js": mainModule("reload-main"), + "run.js": commandModule("nsmreload|run", "reload-run"), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + await extensibilityService.loadExtension(extensionName); + + assert.equal(getLogger(testInjector).warnOutput, ""); + assert.isOk(testInjector.resolveCommand("nsmreload|run")); + }); + }); + + describe("a manifest declaring no commands to load", () => { + it("loads nothing at all for an empty commands map", async () => { + const extensionName = "nsm-optout-ext"; + writeExtension( + extensionName, + { commands: {} }, + { "main.js": mainModule("optout-main") }, + ); + + const extensibilityService = resolveService(testInjector); + const extensionData = + await extensibilityService.loadExtension(extensionName); + + assert.deepStrictEqual(requiredPaths, []); + assert.deepStrictEqual(capture.loadedModules, []); + assert.notInclude(getLogger(testInjector).traceOutput, DEPRECATION_API); + assert.deepStrictEqual(extensionData.commands, []); + }); + }); + + describe("a module that fails to provide its command", () => { + it("names the extension and the module when the module throws", async () => { + const extensionName = "nsm-throwing-ext"; + writeExtension( + extensionName, + { commands: { "nsmthrowing|run": "./run.js" } }, + { + "main.js": mainModule("throwing-main"), + "run.js": `throw new Error("kaboom");`, + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.throws( + () => testInjector.resolveCommand("nsmthrowing|run"), + /nsmthrowing\|run[\s\S]*nsm-throwing-ext[\s\S]*run\.js[\s\S]*kaboom/, + ); + }); + + it("names the extension and the module when the module registers nothing", async () => { + const extensionName = "nsm-silent-ext"; + writeExtension( + extensionName, + { commands: { "nsmsilent|run": "./run.js" } }, + { + "main.js": mainModule("silent-main"), + "run.js": `module.exports = { notADefinition: true };`, + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.throws( + () => testInjector.resolveCommand("nsmsilent|run"), + /nsmsilent\|run[\s\S]*nsm-silent-ext[\s\S]*run\.js/, + ); + }); + }); + + describe("default commands", () => { + it("registers the default before its siblings whatever the key order", async () => { + const extensionName = "nsm-defaults-ext"; + writeExtension( + extensionName, + { + commands: { + "nsmdefaults|other": "./other.js", + "nsmdefaults|*default": "./default.js", + }, + }, + { + "main.js": mainModule("defaults-main"), + "other.js": commandModule("nsmdefaults|other", "defaults-other"), + "default.js": commandModule( + "nsmdefaults|*default", + "defaults-default", + ), + }, + ); + + const extensibilityService = resolveService(testInjector); + await extensibilityService.loadExtension(extensionName); + + assert.equal(getLogger(testInjector).warnOutput, ""); + assert.deepEqual(testInjector.getChildrenCommandsNames("nsmdefaults"), [ + "*default", + "other", + ]); + assert.isOk(testInjector.resolveCommand("nsmdefaults|*default")); + assert.deepEqual(capture.loadedModules, ["defaults-default"]); + }); + }); +}); diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index a8f7a9e1a1..e55c28f3f2 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -241,6 +241,7 @@ function createTestInjector( testInjector.register("tempService", TempServiceStub); testInjector.register("spmService", { applySPMPackages: () => Promise.resolve(), + ensureSPMDependenciesResolved: () => Promise.resolve(), }); return testInjector; @@ -267,7 +268,13 @@ function createPackageJson( .writeJson(join(projectPath, "package.json"), packageJsonData); } -describe("Cocoapods support", () => { +// These suites only define tests on macOS - each body is internally gated on +// darwin. Marking the suite skipped elsewhere is what keeps the runner from +// erroring on an empty suite; an empty suite is only tolerated when skipped. +const describeOnMacOS = + require("os").platform() === "darwin" ? describe : describe.skip; + +describeOnMacOS("Cocoapods support", () => { if (require("os").platform() !== "darwin") { console.log("Skipping Cocoapods tests. They cannot work on windows"); } else { @@ -656,7 +663,7 @@ describe("Cocoapods support", () => { } }); -describe("Source code support", () => { +describeOnMacOS("Source code support", () => { if (require("os").platform() !== "darwin") { console.log( "Skipping Source code in plugin tests. They cannot work on windows", @@ -977,7 +984,7 @@ describe("Source code support", () => { } }); -describe("Static libraries support", () => { +describeOnMacOS("Static libraries support", () => { if (require("os").platform() !== "darwin") { console.log("Skipping static library tests. They work only on darwin."); return; @@ -1035,12 +1042,26 @@ describe("Static libraries support", () => { fs.writeFile(join(staticLibraryHeadersPath, header), ""); }); - iOSProjectService.generateModulemap(staticLibraryHeadersPath, libraryName); + // The modulemap is written into a CLI-managed dir (not next to the + // headers / not in node_modules) and references the headers in place. + const modulemapDir = join(projectPath, ".plugins", libraryName); + const generated = iOSProjectService.generateModulemap( + staticLibraryHeadersPath, + libraryName, + modulemapDir, + ); + assert.isTrue(generated); + // Read the generated modulemap and verify it. - let modulemap = fs.readFile( - join(staticLibraryHeadersPath, "module.modulemap"), + let modulemap = fs.readFile(join(modulemapDir, "module.modulemap")); + const headerCommands = _.map( + headers, + (value) => + `header "${path.relative( + modulemapDir, + join(staticLibraryHeadersPath, value), + )}"`, ); - const headerCommands = _.map(headers, (value) => `header "${value}"`); const modulemapExpectation = `module ${libraryName} { explicit module ${libraryName} { ${headerCommands.join( " ", )} } }`; @@ -1051,13 +1072,16 @@ describe("Static libraries support", () => { _.each(headers, (header) => { fs.deleteFile(join(staticLibraryHeadersPath, header)); }); - iOSProjectService.generateModulemap(staticLibraryHeadersPath, libraryName); + const regenerated = iOSProjectService.generateModulemap( + staticLibraryHeadersPath, + libraryName, + modulemapDir, + ); + assert.isFalse(regenerated); let error: any; try { - modulemap = fs.readFile( - join(staticLibraryHeadersPath, "module.modulemap"), - ); + modulemap = fs.readFile(join(modulemapDir, "module.modulemap")); } catch (err) { error = err; } @@ -1089,7 +1113,7 @@ describe("Relative paths", () => { }); }); -describe("Merge Project XCConfig files", () => { +describeOnMacOS("Merge Project XCConfig files", () => { if (require("os").platform() !== "darwin") { console.log( "Skipping 'Merge Project XCConfig files' tests. They can work only on macOS", @@ -1183,6 +1207,125 @@ describe("Merge Project XCConfig files", () => { } }); + it("The app's build.xcconfig wins over a plugin's", async () => { + fs.writeFile( + appResourcesXcconfigPath, + `CLANG_CXX_LANGUAGE_STANDARD = c++20${EOL}`, + ); + + const pluginPlatformsFolderPath = join(projectPath, "somePlugin", "ios"); + fs.writeFile( + join(pluginPlatformsFolderPath, BUILD_XCCONFIG_FILE_NAME), + `CLANG_CXX_LANGUAGE_STANDARD = c++17${EOL}GCC_C_LANGUAGE_STANDARD = gnu17${EOL}`, + ); + + const pluginsService = testInjector.resolve("pluginsService"); + pluginsService.getAllProductionPlugins = () => [ + { + name: "somePlugin", + pluginPlatformsFolderPath: () => pluginPlatformsFolderPath, + }, + ]; + + await (iOSProjectService).mergeProjectXcconfigFiles(projectData); + + _.each( + xcconfigService.getPluginsXcconfigFilePaths(projectRoot), + (destinationFilePath) => { + assertPropertyValues( + { + // The app pinned this, so the plugin's c++17 must not win. + CLANG_CXX_LANGUAGE_STANDARD: "c++20", + // Keys the app says nothing about still come from the plugin. + GCC_C_LANGUAGE_STANDARD: "gnu17", + }, + destinationFilePath, + testInjector, + ); + }, + ); + }); + + it("Accumulates $(inherited) settings across the app, the plugins and the pods", async () => { + fs.writeFile( + appResourcesXcconfigPath, + `HEADER_SEARCH_PATHS = $(inherited) "$(SRCROOT)/AppHeaders"${EOL}`, + ); + + const pluginPlatformsFolderPath = join(projectPath, "somePlugin", "ios"); + fs.writeFile( + join(pluginPlatformsFolderPath, BUILD_XCCONFIG_FILE_NAME), + `HEADER_SEARCH_PATHS = $(inherited) "$(SRCROOT)/PluginHeaders"${EOL}`, + ); + + const pluginsService = testInjector.resolve("pluginsService"); + pluginsService.getAllProductionPlugins = () => [ + { + name: "somePlugin", + pluginPlatformsFolderPath: () => pluginPlatformsFolderPath, + }, + ]; + + await (iOSProjectService).mergeProjectXcconfigFiles(projectData); + + // The pods xcconfig is merged in a later prepare step, once `pod install` + // has produced it. + const podXcconfigPath = join(projectPath, "pods.xcconfig"); + fs.writeFile( + podXcconfigPath, + `HEADER_SEARCH_PATHS = $(inherited) "\${PODS_ROOT}/Headers/Public"${EOL}`, + ); + + for (const destinationFilePath of _.values( + xcconfigService.getPluginsXcconfigFilePaths(projectRoot), + )) { + await xcconfigService.mergeFiles(podXcconfigPath, destinationFilePath); + + assertPropertyValues( + { + HEADER_SEARCH_PATHS: + '$(inherited) "$(SRCROOT)/AppHeaders" "$(SRCROOT)/PluginHeaders" "${PODS_ROOT}/Headers/Public"', + }, + destinationFilePath, + testInjector, + ); + } + }); + + it("The app's build.xcconfig replaces a plugin's list setting when it drops $(inherited)", async () => { + fs.writeFile( + appResourcesXcconfigPath, + `HEADER_SEARCH_PATHS = "$(SRCROOT)/OnlyMine"${EOL}`, + ); + + const pluginPlatformsFolderPath = join(projectPath, "somePlugin", "ios"); + fs.writeFile( + join(pluginPlatformsFolderPath, BUILD_XCCONFIG_FILE_NAME), + `HEADER_SEARCH_PATHS = $(inherited) "$(SRCROOT)/PluginHeaders"${EOL}`, + ); + + const pluginsService = testInjector.resolve("pluginsService"); + pluginsService.getAllProductionPlugins = () => [ + { + name: "somePlugin", + pluginPlatformsFolderPath: () => pluginPlatformsFolderPath, + }, + ]; + + await (iOSProjectService).mergeProjectXcconfigFiles(projectData); + + _.each( + xcconfigService.getPluginsXcconfigFilePaths(projectRoot), + (destinationFilePath) => { + assertPropertyValues( + { HEADER_SEARCH_PATHS: '"$(SRCROOT)/OnlyMine"' }, + destinationFilePath, + testInjector, + ); + }, + ); + }); + it("Adds the entitlements property if not set by the user", async () => { for (const release in [true, false]) { const realExistsFunction = testInjector.resolve("fs").exists; diff --git a/test/local-cli-delegation.ts b/test/local-cli-delegation.ts new file mode 100644 index 0000000000..3dc8f8bd50 --- /dev/null +++ b/test/local-cli-delegation.ts @@ -0,0 +1,130 @@ +import { assert } from "chai"; +import { spawnSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +// Drives the real bin entry in a child process: delegation must happen before +// any of lib/ loads, so it can only be observed from the outside. +const repoRoot = path.join(__dirname, "..", ".."); +const cliEntry = path.join(repoRoot, "bin", "nativescript.js"); +const ownVersion = JSON.parse( + fs.readFileSync(path.join(repoRoot, "package.json")).toString(), +).version; + +const LOCAL_MARKER = "LOCAL_CLI_RAN"; + +describe("project-local CLI delegation", () => { + let projectDir: string; + + const makeProject = (options?: { + localCli?: boolean; + symlinkToOwnCopy?: boolean; + }): void => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-localcli-")); + fs.writeFileSync( + path.join(projectDir, "package.json"), + JSON.stringify({ name: "test-app", version: "1.0.0" }), + ); + + if (options && options.symlinkToOwnCopy) { + fs.mkdirSync(path.join(projectDir, "node_modules"), { recursive: true }); + fs.symlinkSync( + repoRoot, + path.join(projectDir, "node_modules", "nativescript"), + "junction", + ); + return; + } + + if (options && options.localCli) { + const packageDir = path.join(projectDir, "node_modules", "nativescript"); + fs.mkdirSync(path.join(packageDir, "bin"), { recursive: true }); + fs.writeFileSync( + path.join(packageDir, "package.json"), + JSON.stringify({ name: "nativescript", version: "99.0.0-local" }), + ); + fs.writeFileSync( + path.join(packageDir, "bin", "tns"), + `console.log("${LOCAL_MARKER} delegated=" + process.env.NS_CLI_LOCAL_DELEGATED);`, + ); + } + }; + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + const runCli = ( + args: string[] = ["--version"], + envOverrides: { [key: string]: string } = {}, + ) => { + const env: any = { ...process.env, ...envOverrides }; + delete env.NS_CLI_LOCAL_DELEGATED; + delete env.NS_CLI_NO_LOCAL; + for (const key of Object.keys(envOverrides)) { + env[key] = envOverrides[key]; + } + return spawnSync(process.execPath, [cliEntry, ...args], { + cwd: projectDir, + encoding: "utf8", + env, + }); + }; + + it("hands off to a project-local install, marking the delegated process", () => { + makeProject({ localCli: true }); + + const result = runCli(); + + assert.include(result.stdout, `${LOCAL_MARKER} delegated=1`); + assert.include(result.stderr, "project-local nativescript@99.0.0-local"); + assert.notInclude(result.stdout, ownVersion); + }); + + it("runs the invoked copy when the project has no local install", () => { + makeProject(); + + const result = runCli(); + + assert.include(result.stdout, ownVersion); + assert.notInclude(result.stdout, LOCAL_MARKER); + assert.notInclude(result.stderr, "project-local"); + }); + + it("does not delegate to a symlink of the same copy (npm link)", () => { + makeProject({ symlinkToOwnCopy: true }); + + const result = runCli(); + + assert.include(result.stdout, ownVersion); + assert.notInclude(result.stderr, "project-local"); + }); + + it("--no-local-cli opts out and is stripped before option parsing", () => { + makeProject({ localCli: true }); + + const result = runCli(["--version", "--no-local-cli"]); + + assert.include(result.stdout, ownVersion); + assert.notInclude(result.stdout, LOCAL_MARKER); + }); + + it("NS_CLI_NO_LOCAL opts out", () => { + makeProject({ localCli: true }); + + const result = runCli(["--version"], { NS_CLI_NO_LOCAL: "1" }); + + assert.include(result.stdout, ownVersion); + assert.notInclude(result.stdout, LOCAL_MARKER); + }); + + it("a delegated process never delegates again", () => { + makeProject({ localCli: true }); + + const result = runCli(["--version"], { NS_CLI_LOCAL_DELEGATED: "1" }); + + assert.include(result.stdout, ownVersion); + assert.notInclude(result.stdout, LOCAL_MARKER); + }); +}); diff --git a/test/options.ts b/test/options.ts index e5c5f5e8a5..e6e2557fd1 100644 --- a/test/options.ts +++ b/test/options.ts @@ -6,12 +6,13 @@ import { IOptions } from "../lib/declarations"; import { IInjector } from "../lib/common/definitions/yok"; import { IConfigurationSettings, - OptionType, ISettingsService, } from "../lib/common/declarations"; +import { OptionType } from "../lib/common/enums"; import * as _ from "lodash"; let isExecutionStopped = false; +let warnings: string[] = []; function createTestInjector(): IInjector { const testInjector = new Yok(); @@ -23,6 +24,11 @@ function createTestInjector(): IInjector { setSettings: (settings: IConfigurationSettings): any => undefined, getProfileDir: () => "profileDir", }); + testInjector.register("logger", { + warn: (message: string): void => { + warnings.push(message); + }, + }); return testInjector; } @@ -33,8 +39,6 @@ function createOptions(testInjector: IInjector): IOptions { } describe("options", () => { - // TODO: Igor and Nathan will make this work again - return; let testInjector: IInjector; beforeEach(() => { testInjector = createTestInjector(); @@ -49,6 +53,14 @@ describe("options", () => { testInjector.register("errors", errors); isExecutionStopped = false; + warnings = []; + // The assertions below describe the hard-fail behavior, which is opt-in + // while validation is staged. The staging itself is covered separately. + process.env.NS_STRICT_OPTIONS = "error"; + }); + + afterEach(() => { + delete process.env.NS_STRICT_OPTIONS; }); describe("validateOptions", () => { @@ -125,6 +137,24 @@ describe("options", () => { assert.isTrue(isExecutionStopped); }); + it("breaks execution when the timeout option has no value", () => { + process.argv.push("--timeout"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isTrue(isExecutionStopped); + }); + + it("does not break execution when the timeout option has a value", () => { + process.argv.push("--timeout"); + process.argv.push("500"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + }); + it("breaks execution when valid option has value with spaces only", () => { process.argv.push("--path"); process.argv.push(" "); @@ -181,13 +211,13 @@ describe("options", () => { it("converts string value to array when option type is array", () => { const options: any = createOptions(testInjector); - process.argv.push("--config"); + process.argv.push("--test1"); process.argv.push("value"); options.validateOptions({ test1: { type: OptionType.Array } }); process.argv.pop(); process.argv.pop(); assert.isFalse(isExecutionStopped); - assert.deepStrictEqual(["value"], options["config"]); + assert.deepStrictEqual(["value"], options.argv.test1); }); it("does not break execution when valid commandSpecificOptions are passed", () => { @@ -213,9 +243,11 @@ describe("options", () => { }); it("breaks execution when valid array option has value with length 0", () => { - process.argv.push("--config"); + process.argv.push("--test1"); const options = createOptions(testInjector); - options.validateOptions(); + options.validateOptions({ + test1: { type: OptionType.Array, hasSensitiveValue: false }, + }); process.argv.pop(); assert.isTrue(isExecutionStopped); }); @@ -241,12 +273,11 @@ describe("options", () => { const expectedProfileDir = "TestDir"; process.argv.push("--profile-dir"); process.argv.push(expectedProfileDir); - const settingsService = testInjector.resolve( - "settingsService" - ); + const settingsService = + testInjector.resolve("settingsService"); let valuePassedToSetSettings: string; settingsService.setSettings = ( - settings: IConfigurationSettings + settings: IConfigurationSettings, ): any => { valuePassedToSetSettings = settings.profileDir; }; @@ -274,7 +305,7 @@ describe("options", () => { assert.isFalse( isExecutionStopped, "Dashed options should be validated in specific way. Make sure validation allows yargs specific behavior:" + - "Dashed options (profile-dir) are added to yargs.argv in two ways: profile-dir and profileDir" + "Dashed options (profile-dir) are added to yargs.argv in two ways: profile-dir and profileDir", ); }); @@ -288,7 +319,7 @@ describe("options", () => { assert.isFalse( isExecutionStopped, "Dashed options should be validated in specific way. Make sure validation allows yargs specific behavior:" + - "Dashed options (some-dashed-value) are added to yargs.argv in two ways: some-dashed-value and someDashedValue" + "Dashed options (some-dashed-value) are added to yargs.argv in two ways: some-dashed-value and someDashedValue", ); }); @@ -305,9 +336,94 @@ describe("options", () => { assert.isFalse( isExecutionStopped, "Dashed options should be validated in specific way. Make sure validation allows yargs specific behavior:" + - "Dashed options (special-dashed-v) are added to yargs.argv in two ways: special-dashed-v and specialDashedV" + "Dashed options (special-dashed-v) are added to yargs.argv in two ways: special-dashed-v and specialDashedV", ); }); + + // vision-ng and friends are declared with a literal dashed key, so the + // camelCase spelling yargs derives has no declaration of its own. + _.each(["--vision-ng", "--visionNg"], (arg) => { + it(`does not break execution when ${arg} is passed`, () => { + process.argv.push(arg); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + assert.isEmpty(warnings); + }); + }); + }); + + describe("does not report valid options", () => { + it("accepts a negated declared boolean", () => { + process.argv.push("--no-hmr"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + assert.isEmpty(warnings); + assert.isFalse(options.argv.hmr); + }); + + it("accepts dot-notation values of an object option", () => { + process.argv.push("--env.production"); + process.argv.push("--env.sourceMap"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + assert.isEmpty(warnings); + assert.deepStrictEqual(options.argv.env, { + production: true, + sourceMap: true, + }); + }); + }); + + describe("staged reporting", () => { + it("warns instead of failing when an option is not supported", () => { + delete process.env.NS_STRICT_OPTIONS; + process.argv.push("--unknownOption"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + assert.lengthOf(warnings, 1); + assert.include(warnings[0], "'unknownOption' is not supported"); + assert.include(warnings[0], "NS_STRICT_OPTIONS=error"); + }); + + it("fails when an option is not supported and NS_STRICT_OPTIONS=error", () => { + process.env.NS_STRICT_OPTIONS = "error"; + process.argv.push("--unknownOption"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isTrue(isExecutionStopped); + assert.isEmpty(warnings); + }); + + it("warns instead of failing when a string option has no value", () => { + delete process.env.NS_STRICT_OPTIONS; + process.argv.push("--path"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.isFalse(isExecutionStopped); + assert.lengthOf(warnings, 1); + assert.include(warnings[0], "'path' requires non-empty value"); + }); + + it("reports an undeclared negated flag under the spelling that was used", () => { + delete process.env.NS_STRICT_OPTIONS; + process.argv.push("--no-something"); + const options = createOptions(testInjector); + options.validateOptions(); + process.argv.pop(); + assert.lengthOf(warnings, 1); + assert.include(warnings[0], "'no-something' is not supported"); + }); }); }); @@ -350,8 +466,7 @@ describe("options", () => { expectedHmrValue: false, }, { - name: - "should set hmr to false when provided through dashed options from command", + name: "should set hmr to false when provided through dashed options from command", commandSpecificDashedOptions: { hmr: { type: OptionType.Boolean, @@ -362,14 +477,12 @@ describe("options", () => { expectedHmrValue: false, }, { - name: - "should set hmr to false by default when --release option is provided", + name: "should set hmr to false by default when --release option is provided", args: ["--release"], expectedHmrValue: false, }, { - name: - "should set hmr to false by default when --debug-brk option is provided", + name: "should set hmr to false by default when --debug-brk option is provided", args: ["--debugBrk"], expectedHmrValue: false, }, @@ -396,6 +509,7 @@ function createOptionsWithProfileDir(defaultProfileDir?: string): IOptions { const testInjector = new Yok(); testInjector.register("errors", {}); testInjector.register("staticConfig", {}); + testInjector.register("logger", { warn: (): void => undefined }); let valuePassedToSetSettings: string; testInjector.register("settingsService", { setSettings: (settings: IConfigurationSettings): any => { diff --git a/test/project-commands.ts b/test/project-commands.ts index 259c8185df..bb61c67771 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -2,7 +2,7 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { CreateProjectCommand } from "../lib/commands/create-project"; import { StringCommandParameter } from "../lib/common/command-params"; -import * as helpers from "../lib/common/helpers"; +import { setIsInteractive } from "../lib/common/helpers"; import * as constants from "../lib/constants"; import { assert } from "chai"; import { PrompterStub } from "./stubs"; @@ -132,7 +132,7 @@ class ProjectServiceMock implements IProjectService { } async createProject( - projectOptions: IProjectSettings + projectOptions: IProjectSettings, ): Promise { createProjectCalledWithForce = projectOptions.force; selectedTemplateName = projectOptions.template; @@ -220,8 +220,7 @@ describe("Project commands tests", () => { beforeEach(() => { testInjector = createTestInjector(); - // @ts-expect-error - helpers.isInteractive = () => true; + setIsInteractive(() => true); isProjectCreated = false; validateProjectCallsCount = 0; createProjectCalledWithForce = false; @@ -230,6 +229,10 @@ describe("Project commands tests", () => { createProjectCommand = testInjector.resolve("$createCommand"); }); + afterEach(() => { + setIsInteractive(); + }); + describe("#CreateProjectCommand", () => { it("should not fail when using only --ng.", async () => { options.ng = true; @@ -366,7 +369,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-hello-world-ng" + "@nativescript/template-hello-world-ng", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); @@ -382,7 +385,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-drawer-navigation-ts" + "@nativescript/template-drawer-navigation-ts", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); @@ -398,7 +401,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-tab-navigation" + "@nativescript/template-tab-navigation", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); @@ -414,7 +417,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-drawer-navigation-vue" + "@nativescript/template-drawer-navigation-vue", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); @@ -430,7 +433,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-blank-react" + "@nativescript/template-blank-react", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); @@ -446,7 +449,7 @@ describe("Project commands tests", () => { assert.deepStrictEqual( selectedTemplateName, - "@nativescript/template-blank-svelte" + "@nativescript/template-blank-svelte", ); assert.equal(validateProjectCallsCount, 1); assert.isTrue(createProjectCalledWithForce); diff --git a/test/project-data.ts b/test/project-data.ts index 5b8c747bd1..8551eedbad 100644 --- a/test/project-data.ts +++ b/test/project-data.ts @@ -59,6 +59,7 @@ describe("projectData", () => { bundlerConfigPath?: string; projectName?: string; bundler?: string; + buildPath?: string; }; }): IProjectData => { const testInjector = createTestInjector(); @@ -96,6 +97,36 @@ describe("projectData", () => { return projectData; }; + describe("buildPath", () => { + it("defaults to the platforms directory", () => { + const projectData = prepareTest(); + + assert.deepStrictEqual( + projectData.getBuildRelativeDirectoryPath(), + "platforms", + ); + assert.deepStrictEqual( + projectData.platformsDir, + path.join(projectDir, "platforms"), + ); + }); + + it("is read from the project config", () => { + const projectData = prepareTest({ + configData: { buildPath: "build/native" }, + }); + + assert.deepStrictEqual( + projectData.getBuildRelativeDirectoryPath(), + "build/native", + ); + assert.deepStrictEqual( + projectData.platformsDir, + path.join(projectDir, "build/native"), + ); + }); + }); + describe("projectType", () => { const assertProjectType = ( dependencies: any, diff --git a/test/project-name-service.ts b/test/project-name-service.ts index cec999950b..10ec8b10ed 100644 --- a/test/project-name-service.ts +++ b/test/project-name-service.ts @@ -1,9 +1,10 @@ import { Yok } from "../lib/common/yok"; -import { ProjectNameService } from "../lib/services/project-name-service"; +import { ProjectNameServiceImpl as ProjectNameService } from "../lib/services/project-name-service"; import { assert } from "chai"; import { ErrorsStub, LoggerStub } from "./stubs"; import { IProjectNameService } from "../lib/declarations"; import { IInjector } from "../lib/common/definitions/yok"; +import { setIsInteractive } from "../lib/common/helpers"; import * as _ from "lodash"; const mockProjectNameValidator = { @@ -36,14 +37,20 @@ describe("Project Name Service Tests", () => { const invalidProjectNames = ["1invalid", "app"]; beforeEach(() => { + // ensureValidName only prompts when interactive; without this the result + // depends on whether the runner happens to own a TTY. + setIsInteractive(() => true); testInjector = createTestInjector(); projectNameService = testInjector.resolve("projectNameService"); }); + afterEach(() => { + setIsInteractive(); + }); + it("returns correct name when valid name is entered", async () => { - const actualProjectName = await projectNameService.ensureValidName( - validProjectName - ); + const actualProjectName = + await projectNameService.ensureValidName(validProjectName); assert.deepStrictEqual(actualProjectName, validProjectName); }); @@ -67,9 +74,8 @@ describe("Project Name Service Tests", () => { } }; - const actualProjectName = await projectNameService.ensureValidName( - invalidProjectName - ); + const actualProjectName = + await projectNameService.ensureValidName(invalidProjectName); assert.deepStrictEqual(actualProjectName, validProjectName); }); @@ -77,7 +83,7 @@ describe("Project Name Service Tests", () => { it(`returns the invalid name when "${invalidProjectName}" is entered and --force flag is present`, async () => { const actualProjectName = await projectNameService.ensureValidName( validProjectName, - { force: true } + { force: true }, ); assert.deepStrictEqual(actualProjectName, validProjectName); diff --git a/test/services/analytics/analytics-service.ts b/test/services/analytics/analytics-service.ts index 88f08320d6..3246e81e21 100644 --- a/test/services/analytics/analytics-service.ts +++ b/test/services/analytics/analytics-service.ts @@ -11,8 +11,10 @@ import { IInjector } from "../../../lib/common/definitions/yok"; import { IChildProcess, IAnalyticsService, - GoogleAnalyticsDataType, } from "../../../lib/common/declarations"; +import { GoogleAnalyticsDataType } from "../../../lib/common/enums"; +import { DetachedProcessMessages } from "../../../lib/detached-processes/detached-process-enums"; +import { GoogleAnalyticsCustomDimensions } from "../../../lib/common/services/analytics/google-analytics-custom-dimensions"; const helpers = require("../../../lib/common/helpers"); const originalIsInteractive = helpers.isInteractive; @@ -59,8 +61,8 @@ const createTestInjector = (opts?: { "projectHelper", new stubs.ProjectHelperStub( opts && opts.projectHelperErrorMsg, - opts && opts.projectDir - ) + opts && opts.projectDir, + ), ); return testInjector; @@ -82,20 +84,20 @@ describe("analyticsService", () => { }; }) => { const testInjector = createTestInjector(); - const staticConfig = testInjector.resolve( - "staticConfig" - ); + const staticConfig = + testInjector.resolve("staticConfig"); staticConfig.disableAnalytics = configuration.disableAnalytics; - configuration.userSettingsServiceOpts = configuration.userSettingsServiceOpts || { - trackFeatureUsageValue: "false", - defaultValue: "true", - }; + configuration.userSettingsServiceOpts = + configuration.userSettingsServiceOpts || { + trackFeatureUsageValue: "false", + defaultValue: "true", + }; const userSettingsService = testInjector.resolve( - "userSettingsService" + "userSettingsService", ); userSettingsService.getSettingValue = async ( - settingName: string + settingName: string, ): Promise => { if (settingName === trackFeatureUsage) { return configuration.userSettingsServiceOpts.trackFeatureUsageValue; @@ -105,20 +107,18 @@ describe("analyticsService", () => { }; let isChildProcessSpawned = false; - const childProcess = testInjector.resolve( - "childProcess" - ); + const childProcess = + testInjector.resolve("childProcess"); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { isChildProcessSpawned = true; }; - const analyticsService = testInjector.resolve( - AnalyticsService - ); + const analyticsService = + testInjector.resolve(AnalyticsService); await analyticsService.trackInGoogleAnalytics({ googleAnalyticsDataType: GoogleAnalyticsDataType.Page, customDimensions: { @@ -169,11 +169,10 @@ describe("analyticsService", () => { describe("does not fail", () => { const assertExpectedError = async ( testInjector: IInjector, - opts: { isChildProcessSpawned: boolean; expectedErrorMessage: string } + opts: { isChildProcessSpawned: boolean; expectedErrorMessage: string }, ) => { - const analyticsService = testInjector.resolve( - AnalyticsService - ); + const analyticsService = + testInjector.resolve(AnalyticsService); await analyticsService.trackInGoogleAnalytics({ googleAnalyticsDataType: GoogleAnalyticsDataType.Page, customDimensions: { @@ -185,13 +184,13 @@ describe("analyticsService", () => { const logger = testInjector.resolve("logger"); assert.isTrue( logger.traceOutput.indexOf(opts.expectedErrorMessage) !== -1, - `Tried to find error '${opts.expectedErrorMessage}', but couldn't. logger's trace output is: ${logger.traceOutput}` + `Tried to find error '${opts.expectedErrorMessage}', but couldn't. logger's trace output is: ${logger.traceOutput}`, ); }; const setupTest = ( expectedErrorMessage: string, - projectHelperErrorMsg?: string + projectHelperErrorMsg?: string, ): any => { const testInjector = createTestInjector({ projectHelperErrorMsg }); const opts = { @@ -199,9 +198,8 @@ describe("analyticsService", () => { expectedErrorMessage, }; - const childProcess = testInjector.resolve( - "childProcess" - ); + const childProcess = + testInjector.resolve("childProcess"); return { testInjector, opts, @@ -211,12 +209,12 @@ describe("analyticsService", () => { it("when unable to start broker process", async () => { const { testInjector, childProcess, opts } = setupTest( - "Unable to get broker instance due to error: Error: custom error" + "Unable to get broker instance due to error: Error: custom error", ); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; throw new Error("custom error"); @@ -227,13 +225,13 @@ describe("analyticsService", () => { it("when broker cannot start for required timeout", async () => { const { testInjector, childProcess, opts } = setupTest( - "Unable to get broker instance due to error: Error: Unable to start Analytics Broker process." + "Unable to get broker instance due to error: Error: Unable to start Analytics Broker process.", ); const originalSetTimeout = setTimeout; childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; (global).setTimeout = ( @@ -251,13 +249,13 @@ describe("analyticsService", () => { it("when broker is not connected", async () => { const { testInjector, childProcess, opts } = setupTest( - "Broker not found or not connected." + "Broker not found or not connected.", ); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; const spawnedProcess: any = getSpawnedProcess(); @@ -268,9 +266,9 @@ describe("analyticsService", () => { () => spawnedProcess.emit( "message", - DetachedProcessMessages.ProcessReadyToReceive + DetachedProcessMessages.ProcessReadyToReceive, ), - 1 + 1, ); return spawnedProcess; }; @@ -280,13 +278,13 @@ describe("analyticsService", () => { it("when sending message fails", async () => { const { testInjector, childProcess, opts } = setupTest( - "Error while trying to send message to broker: Error: Failed to sent data." + "Error while trying to send message to broker: Error: Failed to sent data.", ); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; const spawnedProcess: any = getSpawnedProcess(); @@ -300,9 +298,9 @@ describe("analyticsService", () => { () => spawnedProcess.emit( "message", - DetachedProcessMessages.ProcessReadyToReceive + DetachedProcessMessages.ProcessReadyToReceive, ), - 1 + 1, ); return spawnedProcess; }; @@ -314,12 +312,12 @@ describe("analyticsService", () => { const projectHelperErrorMsg = "Failed to find project directory."; const { testInjector, childProcess, opts } = setupTest( `Unable to get the projectDir from projectHelper Error: ${projectHelperErrorMsg}`, - projectHelperErrorMsg + projectHelperErrorMsg, ); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; const spawnedProcess: any = getSpawnedProcess(); @@ -334,9 +332,9 @@ describe("analyticsService", () => { () => spawnedProcess.emit( "message", - DetachedProcessMessages.ProcessReadyToReceive + DetachedProcessMessages.ProcessReadyToReceive, ), - 1 + 1, ); return spawnedProcess; }; @@ -350,7 +348,7 @@ describe("analyticsService", () => { expectedResult: any, dataToSend: any, terminalOpts?: { isInteractive: boolean }, - projectHelperOpts?: { projectDir: string } + projectHelperOpts?: { projectDir: string }, ): { testInjector: IInjector; opts: any } => { helpers.isInteractive = () => terminalOpts ? terminalOpts.isInteractive : true; @@ -363,13 +361,12 @@ describe("analyticsService", () => { messageSent: null, }; - const childProcess = testInjector.resolve( - "childProcess" - ); + const childProcess = + testInjector.resolve("childProcess"); childProcess.spawn = ( command: string, args?: string[], - options?: any + options?: any, ): any => { opts.isChildProcessSpawned = true; const spawnedProcess: any = getSpawnedProcess(); @@ -384,9 +381,9 @@ describe("analyticsService", () => { () => spawnedProcess.emit( "message", - DetachedProcessMessages.ProcessReadyToReceive + DetachedProcessMessages.ProcessReadyToReceive, ), - 1 + 1, ); return spawnedProcess; @@ -405,11 +402,10 @@ describe("analyticsService", () => { expectedResult: any; messageSent: any; dataToSend: any; - } + }, ) => { - const analyticsService = testInjector.resolve( - AnalyticsService - ); + const analyticsService = + testInjector.resolve(AnalyticsService); await analyticsService.trackInGoogleAnalytics(opts.dataToSend); assert.isTrue(opts.isChildProcessSpawned); @@ -426,7 +422,7 @@ describe("analyticsService", () => { const getExpectedResult = ( gaDataType: string, analyticsClient?: string, - projectType?: string + projectType?: string, ): any => { const expectedResult: any = { type: "googleAnalyticsData", @@ -456,7 +452,7 @@ describe("analyticsService", () => { it(`when data is ${googleAnalyticsDataType}`, async () => { const { testInjector, opts } = setupTest( getExpectedResult(googleAnalyticsDataType), - getDataToSend(googleAnalyticsDataType) + getDataToSend(googleAnalyticsDataType), ); await assertExpectedResult(testInjector, opts); }); @@ -466,11 +462,11 @@ describe("analyticsService", () => { getExpectedResult( googleAnalyticsDataType, null, - sampleProjectType + sampleProjectType, ), getDataToSend(googleAnalyticsDataType), null, - { projectDir: "/some-dir" } + { projectDir: "/some-dir" }, ); await assertExpectedResult(testInjector, opts); }); @@ -479,10 +475,10 @@ describe("analyticsService", () => { const { testInjector, opts } = setupTest( getExpectedResult( googleAnalyticsDataType, - AnalyticsClients.Unknown + AnalyticsClients.Unknown, ), getDataToSend(googleAnalyticsDataType), - { isInteractive: false } + { isInteractive: false }, ); await assertExpectedResult(testInjector, opts); }); @@ -496,7 +492,7 @@ describe("analyticsService", () => { const { testInjector, opts } = setupTest( getExpectedResult(googleAnalyticsDataType, analyticsClient), getDataToSend(googleAnalyticsDataType), - { isInteractive } + { isInteractive }, ); const options = testInjector.resolve("options"); options.analyticsClient = analyticsClient; @@ -504,7 +500,7 @@ describe("analyticsService", () => { await assertExpectedResult(testInjector, opts); }); }); - } + }, ); }); }); diff --git a/test/services/analytics/google-analytics-provider.ts b/test/services/analytics/google-analytics-provider.ts new file mode 100644 index 0000000000..1e74e4dd0c --- /dev/null +++ b/test/services/analytics/google-analytics-provider.ts @@ -0,0 +1,211 @@ +import { assert } from "chai"; +import { GoogleAnalyticsProvider } from "../../../lib/services/analytics/google-analytics-provider"; +import { GoogleAnalyticsDataType } from "../../../lib/common/enums"; +import { GoogleAnalyticsCustomDimensions } from "../../../lib/common/services/analytics/google-analytics-custom-dimensions"; +import * as stubs from "../../stubs"; + +const clientId = "test-client-id"; +const measurementId = "G-TESTID"; +const apiSecret = "test-secret"; + +interface ISentRequest { + url: string; + method: string; + headers: any; + body: any; +} + +const createProvider = (opts?: { + measurementId?: string; + apiSecret?: string; +}) => { + const requests: ISentRequest[] = []; + + const provider = new GoogleAnalyticsProvider( + clientId, + { version: "9.0.0" }, + { getUserAgentString: (proto: string) => `${proto} (test)` }, + new stubs.LoggerStub(), + { getCache: async (): Promise => null }, + { + GA_MEASUREMENT_ID: + "measurementId" in (opts || {}) ? opts.measurementId : measurementId, + GA_API_SECRET: "apiSecret" in (opts || {}) ? opts.apiSecret : apiSecret, + }, + { + httpRequest: async (options: any) => { + requests.push({ + url: options.url, + method: options.method, + headers: options.headers, + body: JSON.parse(options.body), + }); + return {}; + }, + }, + { logData: (): void => undefined }, + ); + + return { provider, requests }; +}; + +describe("GoogleAnalyticsProvider", () => { + it("posts a command event to the measurement protocol", async () => { + const { provider, requests } = createProvider(); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Page, + path: "build android", + title: "build android", + }); + + assert.lengthOf(requests, 1); + const [request] = requests; + + assert.strictEqual(request.method, "POST"); + assert.include(request.url, "measurement_id=G-TESTID"); + assert.include(request.url, "api_secret=test-secret"); + assert.strictEqual(request.headers["Content-Type"], "application/json"); + assert.strictEqual(request.body.client_id, clientId); + assert.isTrue(request.body.non_personalized_ads); + assert.lengthOf(request.body.events, 1); + assert.strictEqual(request.body.events[0].name, "command"); + assert.strictEqual( + request.body.events[0].params.command_name, + "build android", + ); + }); + + it("attributes events to the command that is running", async () => { + const { provider, requests } = createProvider(); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Page, + path: "build android", + title: "build android", + }); + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: "Build", + label: "android", + }); + + assert.strictEqual( + requests[1].body.events[0].params.command_name, + "build android", + ); + }); + + it("names the event after the action and carries category and label", async () => { + const { provider, requests } = createProvider(); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + category: "CLI", + action: "Build", + label: "android", + value: 3, + }); + + const [event] = requests[0].body.events; + assert.strictEqual(event.name, "Build"); + assert.strictEqual(event.params.event_category, "CLI"); + assert.strictEqual(event.params.event_label, "android"); + assert.strictEqual(event.params.value, 3); + }); + + it("translates custom dimension slots into named parameters", async () => { + const { provider, requests } = createProvider(); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: "Build", + label: "android", + customDimensions: { + [GoogleAnalyticsCustomDimensions.projectType]: "Shared", + }, + }); + + const { params } = requests[0].body.events[0]; + assert.strictEqual(params.project_type, "Shared"); + assert.strictEqual(params.cli_version, "9.0.0"); + assert.strictEqual(params.node_version, process.version); + assert.strictEqual(params.client_uuid, clientId); + assert.isString(params.session_id); + // the raw cdN slot names must not survive into the payload + assert.notProperty(params, GoogleAnalyticsCustomDimensions.projectType); + assert.notProperty(params, GoogleAnalyticsCustomDimensions.cliVersion); + }); + + it("sanitizes action names that are not valid event names", async () => { + const cases: [string, string][] = [ + ["Build android", "Build_android"], + ["1nvalid-start", "nvalid_start"], + ["", "cli_event"], + ["a".repeat(60), "a".repeat(40)], + ]; + + for (const [action, expected] of cases) { + const { provider, requests } = createProvider(); + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action, + label: "l", + }); + + assert.strictEqual(requests[0].body.events[0].name, expected, action); + } + }); + + it("omits dimensions that have no value", async () => { + const { provider, requests } = createProvider(); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: "Build", + label: "android", + }); + + const { params } = requests[0].body.events[0]; + // projectType and isShared default to null and must not be sent + assert.notProperty(params, "project_type"); + assert.notProperty(params, "is_shared"); + }); + + it("sends nothing when analytics is not configured", async () => { + for (const opts of [{ measurementId: "" }, { apiSecret: "" }]) { + const { provider, requests } = createProvider(opts); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: "Build", + label: "android", + }); + + assert.lengthOf(requests, 0); + } + }); + + it("swallows transport failures", async () => { + const provider = new GoogleAnalyticsProvider( + clientId, + { version: "9.0.0" }, + { getUserAgentString: (proto: string) => proto }, + new stubs.LoggerStub(), + { getCache: async (): Promise => null }, + { GA_MEASUREMENT_ID: measurementId, GA_API_SECRET: apiSecret }, + { + httpRequest: async (): Promise => { + throw new Error("network down"); + }, + }, + { logData: (): void => undefined }, + ); + + await provider.trackHit({ + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: "Build", + label: "android", + }); + }); +}); diff --git a/test/services/android-bundle-tool-service.ts b/test/services/android-bundle-tool-service.ts new file mode 100644 index 0000000000..370eb28719 --- /dev/null +++ b/test/services/android-bundle-tool-service.ts @@ -0,0 +1,239 @@ +import { join } from "path"; +import { assert } from "chai"; +import { Yok } from "../../lib/common/yok"; +import { IInjector } from "../../lib/common/definitions/yok"; +import { AndroidBundleToolService } from "../../lib/services/android/android-bundle-tool-service"; +import { IAndroidBundleToolService } from "../../lib/definitions/android-bundle-tool-service"; +import { + ErrorsStub, + FileSystemStub, + LoggerStub, + TerminalSpinnerServiceStub, +} from "../stubs"; +import { + BUNDLETOOL_PATH_ENV_VAR, + BUNDLETOOL_SHA256, + BUNDLETOOL_VERSION, +} from "../../lib/constants"; + +describe("androidBundleToolService", () => { + const profileDir = join("/", "profile"); + const cacheDir = join(profileDir, "bundletool"); + const jarPath = join(cacheDir, `bundletool-all-${BUNDLETOOL_VERSION}.jar`); + const tempPath = `${jarPath}.download`; + const deviceId = "emulator-5554"; + + let originalEnvValue: string; + + beforeEach(() => { + originalEnvValue = process.env[BUNDLETOOL_PATH_ENV_VAR]; + delete process.env[BUNDLETOOL_PATH_ENV_VAR]; + }); + + afterEach(() => { + if (originalEnvValue === undefined) { + delete process.env[BUNDLETOOL_PATH_ENV_VAR]; + } else { + process.env[BUNDLETOOL_PATH_ENV_VAR] = originalEnvValue; + } + }); + + const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + + testInjector.register("childProcess", { + spawnedArgs: null, + trySpawnFromCloseEvent(command: string, args: string[]) { + this.spawnedArgs = args; + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }, + }); + testInjector.register("sysInfo", { + getJavaPath: async () => "java", + }); + testInjector.register("errors", ErrorsStub); + testInjector.register("fs", FileSystemStub); + testInjector.register("httpClient", { + requests: [], + async httpRequest(options: any) { + this.requests.push(options); + return {}; + }, + }); + testInjector.register("lockService", { + lockedActions: 0, + executeActionWithLock(action: () => Promise): Promise { + this.lockedActions++; + return action(); + }, + }); + testInjector.register("logger", LoggerStub); + testInjector.register("settingsService", { + getProfileDir: () => profileDir, + }); + testInjector.register("terminalSpinnerService", TerminalSpinnerServiceStub); + testInjector.register("androidBundleToolService", AndroidBundleToolService); + + return testInjector; + }; + + // the jar path is resolved lazily on the first bundletool invocation, so the + // arguments handed to java are what the resolution actually produced + const getJarPassedToJava = (testInjector: IInjector): string => { + const childProcess = testInjector.resolve("childProcess"); + const jarFlagIndex = childProcess.spawnedArgs.indexOf("-jar"); + + return childProcess.spawnedArgs[jarFlagIndex + 1]; + }; + + const installApks = (testInjector: IInjector): Promise => { + const service = testInjector.resolve( + "androidBundleToolService", + ); + + return service.installApks({ apksFilePath: "my.apks", deviceId }); + }; + + describe("resolving bundletool", () => { + it("uses the jar pointed at by the env var without downloading", async () => { + const customPath = join("/", "opt", "bundletool.jar"); + process.env[BUNDLETOOL_PATH_ENV_VAR] = customPath; + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + + await installApks(testInjector); + + assert.equal(getJarPassedToJava(testInjector), customPath); + assert.lengthOf(httpClient.requests, 0); + }); + + it("fails when the env var points at a missing file", async () => { + const customPath = join("/", "opt", "missing.jar"); + process.env[BUNDLETOOL_PATH_ENV_VAR] = customPath; + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = () => false; + + await assert.isRejected( + installApks(testInjector), + `${BUNDLETOOL_PATH_ENV_VAR} is set to "${customPath}", but no file exists there.`, + ); + }); + + it("reuses the cached jar when its checksum matches", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + fs.exists = (path: string) => path === jarPath; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + + await installApks(testInjector); + + assert.equal(getJarPassedToJava(testInjector), jarPath); + assert.lengthOf(httpClient.requests, 0); + }); + + it("resolves the jar only once across multiple invocations", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + let shasumCalls = 0; + fs.exists = (path: string) => path === jarPath; + fs.getFileShasum = async () => { + shasumCalls++; + return BUNDLETOOL_SHA256; + }; + + await installApks(testInjector); + await installApks(testInjector); + + assert.equal(shasumCalls, 1); + }); + }); + + describe("downloading bundletool", () => { + it("downloads, verifies and atomically moves the jar into the cache", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + const renames: { from: string; to: string }[] = []; + fs.exists = () => false; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + fs.rename = (from: string, to: string) => { + renames.push({ from, to }); + }; + + await installApks(testInjector); + + assert.lengthOf(httpClient.requests, 1); + assert.equal( + httpClient.requests[0].url, + `https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar`, + ); + assert.deepEqual(renames, [{ from: tempPath, to: jarPath }]); + assert.equal(getJarPassedToJava(testInjector), jarPath); + }); + + it("takes the lock and re-checks the cache before downloading", async () => { + const testInjector = createTestInjector(); + const lockService = testInjector.resolve("lockService"); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + // absent when first checked, present by the time the lock is acquired, + // as if another process downloaded it while this one waited + let existsCalls = 0; + fs.exists = () => existsCalls++ > 0; + fs.getFileShasum = async () => BUNDLETOOL_SHA256; + + await installApks(testInjector); + + assert.equal(lockService.lockedActions, 1); + assert.lengthOf(httpClient.requests, 0); + assert.equal(getJarPassedToJava(testInjector), jarPath); + }); + + it("deletes the partial download and fails on a checksum mismatch", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = () => false; + fs.getFileShasum = async () => "deadbeef"; + + await assert.isRejected( + installApks(testInjector), + /Checksum mismatch for bundletool/, + ); + assert.include(fs.deletedFiles, tempPath); + }); + + it("deletes the partial download and points at the env var when the download fails", async () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const httpClient = testInjector.resolve("httpClient"); + fs.exists = () => false; + httpClient.httpRequest = async () => { + throw new Error("socket hang up"); + }; + + await assert.isRejected( + installApks(testInjector), + new RegExp( + `Unable to download bundletool.*${BUNDLETOOL_PATH_ENV_VAR}.*socket hang up`, + ), + ); + assert.include(fs.deletedFiles, tempPath); + }); + + it("discards a cached jar whose checksum no longer matches", async () => { + const testInjector = createTestInjector(); + const httpClient = testInjector.resolve("httpClient"); + const fs = testInjector.resolve("fs"); + const shasums = ["tampered", "tampered", BUNDLETOOL_SHA256]; + fs.exists = () => shasums.length > 1; + fs.getFileShasum = async () => shasums.shift(); + + await installApks(testInjector); + + assert.include(fs.deletedFiles, jarPath); + assert.lengthOf(httpClient.requests, 1); + }); + }); +}); diff --git a/test/services/android/gradle-build-args-service.ts b/test/services/android/gradle-build-args-service.ts index 3f2a25f0af..8851f19e67 100644 --- a/test/services/android/gradle-build-args-service.ts +++ b/test/services/android/gradle-build-args-service.ts @@ -163,6 +163,56 @@ describe("GradleBuildArgsService", () => { ); }); + describe("gradleFlavor", async () => { + const testCases = [ + { + name: "should build the flavor of a debug build", + buildConfig: { release: false, gradleFlavor: "foo" }, + logLevel: "INFO", + expectedTask: "assembleFooDebug", + }, + { + name: "should build the flavor of a release build", + buildConfig: { ...releaseBuildConfig, gradleFlavor: "foo" }, + logLevel: "INFO", + expectedTask: "assembleFooRelease", + }, + { + name: "should build the flavor of an android bundle", + buildConfig: { + release: false, + androidBundle: true, + gradleFlavor: "foo", + }, + logLevel: "INFO", + expectedTask: "bundleFooDebug", + }, + { + name: "should keep an already capitalized flavor", + buildConfig: { release: false, gradleFlavor: "Foo" }, + logLevel: "INFO", + expectedTask: "assembleFooDebug", + }, + ]; + + for (const testCase of testCases) { + it(testCase.name, async () => { + const injector = createTestInjector(); + const logger = injector.resolve("logger"); + logger.getLevel = () => testCase.logLevel; + + const gradleBuildArgsService = injector.resolve( + "gradleBuildArgsService" + ); + const args = await gradleBuildArgsService.getBuildTaskArgs( + testCase.buildConfig + ); + + assert.deepStrictEqual(args[0], testCase.expectedTask); + }); + } + }); + describe("getCleanTaskArgs", async () => { const testCases = [ { diff --git a/test/services/bundler/bundler-compiler-service.ts b/test/services/bundler/bundler-compiler-service.ts index 49d69a55f4..ba6a0880a6 100644 --- a/test/services/bundler/bundler-compiler-service.ts +++ b/test/services/bundler/bundler-compiler-service.ts @@ -1,9 +1,15 @@ import { Yok } from "../../../lib/common/yok"; import { BundlerCompilerService } from "../../../lib/services/bundler/bundler-compiler-service"; import { assert } from "chai"; +import { EventEmitter } from "events"; +import * as path from "path"; import { ErrorsStub } from "../../stubs"; import { IInjector } from "../../../lib/common/definitions/yok"; -import { CONFIG_FILE_NAME_DISPLAY } from "../../../lib/constants"; +import { + BUNDLER_COMPILATION_COMPLETE, + CONFIG_FILE_NAME_DISPLAY, + PackageManagers, +} from "../../../lib/constants"; const iOSPlatformName = "ios"; const androidPlatformName = "android"; @@ -18,27 +24,39 @@ function getAllEmittedFiles(hash: string) { ]; } -function createTestInjector(): IInjector { +function createTestInjector( + packageManager: PackageManagers = PackageManagers.npm, +): IInjector { const testInjector = new Yok(); testInjector.register("packageManager", { - getPackageManagerName: async () => "npm", + getPackageManagerName: async () => packageManager, }); testInjector.register("bundlerCompilerService", BundlerCompilerService); testInjector.register("childProcess", {}); testInjector.register("hooksService", {}); testInjector.register("hostInfo", {}); testInjector.register("options", {}); - testInjector.register("logger", {}); + testInjector.register("logger", { + info: () => ({}), + trace: () => ({}), + warn: () => ({}), + }); testInjector.register("errors", ErrorsStub); testInjector.register("packageInstallationManager", {}); testInjector.register("mobileHelper", {}); - testInjector.register("cleanupService", {}); + testInjector.register("cleanupService", { + addKillProcess: async () => ({}), + removeKillProcess: async () => ({}), + }); testInjector.register("projectConfigService", { getValue: (key: string, defaultValue?: string) => defaultValue, }); testInjector.register("fs", { exists: (filePath: string) => true, }); + testInjector.register("viteHmrPortService", { + getPort: async () => 5173, + }); return testInjector; } @@ -52,6 +70,29 @@ describe("BundlerCompilerService", () => { bundlerCompilerService = testInjector.resolve(BundlerCompilerService); }); + describe("shouldUsePreserveSymlinksOption", () => { + it("should preserve symlinks for npm", async () => { + const result = await (( + bundlerCompilerService + )).shouldUsePreserveSymlinksOption(); + + assert.isTrue(result); + }); + + for (const packageManager of [PackageManagers.pnpm, PackageManagers.bun]) { + it(`should not preserve symlinks for ${packageManager}`, async () => { + testInjector = createTestInjector(packageManager); + bundlerCompilerService = testInjector.resolve(BundlerCompilerService); + + const result = await (( + bundlerCompilerService + )).shouldUsePreserveSymlinksOption(); + + assert.isFalse(result); + }); + } + }); + describe("getUpdatedEmittedFiles", () => { // backwards compatibility with old versions of nativescript-dev-webpack it("should return only hot updates when nextHash is not provided", async () => { @@ -204,6 +245,102 @@ describe("BundlerCompilerService", () => { }); }); + describe("getViteDistOutputPath", () => { + it("stages each platform in its own directory when NS_VITE_DIST_DIR is unset", () => { + const previous = process.env.NS_VITE_DIST_DIR; + try { + delete process.env.NS_VITE_DIST_DIR; + assert.strictEqual( + (bundlerCompilerService).getViteDistOutputPath( + "/project", + "ios", + ), + path.join("/project", ".ns-vite-build", "ios"), + ); + assert.strictEqual( + (bundlerCompilerService).getViteDistOutputPath( + "/project", + "android", + ), + path.join("/project", ".ns-vite-build", "android"), + ); + } finally { + if (previous === undefined) { + delete process.env.NS_VITE_DIST_DIR; + } else { + process.env.NS_VITE_DIST_DIR = previous; + } + } + }); + + it("uses NS_VITE_DIST_DIR verbatim when set", () => { + const previous = process.env.NS_VITE_DIST_DIR; + try { + process.env.NS_VITE_DIST_DIR = "custom-dist"; + assert.strictEqual( + (bundlerCompilerService).getViteDistOutputPath( + "/project", + "ios", + ), + path.join("/project", "custom-dist"), + ); + } finally { + if (previous === undefined) { + delete process.env.NS_VITE_DIST_DIR; + } else { + process.env.NS_VITE_DIST_DIR = previous; + } + } + }); + }); + + describe("getViteChildEnv", () => { + let previous: string; + beforeEach(() => { + previous = process.env.NS_VITE_DIST_DIR; + delete process.env.NS_VITE_DIST_DIR; + (bundlerCompilerService).getBundler = () => "vite"; + testInjector.resolve("viteHmrPortService").getPort = async ( + platform: string, + ) => (platform === "ios" ? 5173 : 5174); + }); + afterEach(() => { + if (previous === undefined) { + delete process.env.NS_VITE_DIST_DIR; + } else { + process.env.NS_VITE_DIST_DIR = previous; + } + }); + + it("hands HMR sessions the platform's staging dir and resolved port", async () => { + assert.deepEqual( + await (bundlerCompilerService).getViteChildEnv("android", { + watch: true, + hmr: true, + }), + { NS_VITE_DIST_DIR: ".ns-vite-build/android", NS_HMR_PORT: "5174" }, + ); + }); + + it("does not resolve a port for builds that run no dev server", async () => { + assert.deepEqual( + await (bundlerCompilerService).getViteChildEnv("ios", { + watch: true, + hmr: false, + }), + { NS_VITE_DIST_DIR: ".ns-vite-build/ios" }, + ); + assert.deepEqual( + await (bundlerCompilerService).getViteChildEnv("ios", { + watch: true, + hmr: true, + release: true, + }), + { NS_VITE_DIST_DIR: ".ns-vite-build/ios" }, + ); + }); + }); + describe("compileWithWatch", () => { it("fails when the value set for bundlerConfigPath is not existant file", async () => { const bundlerConfigPath = "some path.js"; @@ -218,9 +355,150 @@ describe("BundlerCompilerService", () => { `The bundler configuration file ${bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}`, ); }); + + it("does not emit a live sync event for the initial Vite watch build", async () => { + const platformData = { + platformNameLowerCase: "ios", + appDestinationDirectoryPath: "/platform/app", + }; + const projectData = { + projectDir: "/project", + bundler: "vite", + bundlerConfigPath: "/project/vite.config.ts", + }; + const prepareData = { hmr: false }; + const childProcess = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + pid: number; + }; + + childProcess.stdout = new EventEmitter(); + childProcess.stderr = new EventEmitter(); + childProcess.pid = 123; + + testInjector.resolve("options").hostProjectModuleName = "app"; + (bundlerCompilerService).getBundler = () => "vite"; + (bundlerCompilerService).startBundleProcess = async () => + childProcess; + (bundlerCompilerService).copyViteBundleToNative = () => ({}); + + const emittedEvents: any[] = []; + bundlerCompilerService.on(BUNDLER_COMPILATION_COMPLETE, (data) => { + emittedEvents.push(data); + }); + + const compilePromise = bundlerCompilerService.compileWithWatch( + platformData, + projectData, + prepareData, + ); + await new Promise((resolve) => setImmediate(resolve)); + + childProcess.emit("message", { + emittedFiles: ["bundle.mjs"], + buildType: "initial", + hash: "hash-1", + isHMR: false, + }); + + await compilePromise; + assert.lengthOf(emittedEvents, 0); + + childProcess.emit("message", { + emittedFiles: ["bundle.mjs"], + buildType: "incremental", + hash: "hash-2", + isHMR: false, + }); + + assert.lengthOf(emittedEvents, 1); + assert.deepStrictEqual(emittedEvents[0], { + files: ["/platform/app/app/bundle.mjs"], + hasOnlyHotUpdateFiles: false, + hmrData: { + hash: "hash-2", + fallbackFiles: [], + }, + platform: "ios", + }); + }); }); describe("compileWithoutWatch", () => { + it("copies a successful Vite build to the native app", async () => { + const previous = process.env.NS_VITE_DIST_DIR; + delete process.env.NS_VITE_DIST_DIR; + try { + const childProcess = Object.assign(new EventEmitter(), { pid: 1234 }); + const copies: Array<{ + distOutput: string; + destDir: string; + failOnError: boolean; + }> = []; + testInjector.resolve("options").hostProjectModuleName = "app"; + (bundlerCompilerService).getBundler = () => "vite"; + (bundlerCompilerService).startBundleProcess = async () => + childProcess; + (bundlerCompilerService).copyViteBundleToNative = ( + distOutput: string, + destDir: string, + _specificFiles: string[], + failOnError: boolean, + ) => { + copies.push({ distOutput, destDir, failOnError }); + }; + + const compilation = bundlerCompilerService.compileWithoutWatch( + { + platformNameLowerCase: "android", + appDestinationDirectoryPath: "/project/platforms/android", + }, + { projectDir: "/project" }, + {}, + ); + setImmediate(() => childProcess.emit("close", 0)); + await compilation; + + assert.deepEqual(copies, [ + { + distOutput: path.join("/project", ".ns-vite-build", "android"), + destDir: path.join("/project/platforms/android", "app"), + failOnError: true, + }, + ]); + } finally { + if (previous === undefined) { + delete process.env.NS_VITE_DIST_DIR; + } else { + process.env.NS_VITE_DIST_DIR = previous; + } + } + }); + + it("fails when a successful Vite build cannot be copied", async () => { + const childProcess = Object.assign(new EventEmitter(), { pid: 1234 }); + testInjector.resolve("options").hostProjectModuleName = "app"; + (bundlerCompilerService).getBundler = () => "vite"; + (bundlerCompilerService).startBundleProcess = async () => + childProcess; + (bundlerCompilerService).copyViteBundleToNative = () => { + throw new Error("copy failed"); + }; + + const compilation = bundlerCompilerService.compileWithoutWatch( + { + platformNameLowerCase: "ios", + appDestinationDirectoryPath: "/project/platforms/ios", + }, + { projectDir: "/project" }, + {}, + ); + setImmediate(() => childProcess.emit("close", 0)); + + await assert.isRejected(compilation, "copy failed"); + }); + it("fails when the value set for bundlerConfigPath is not existant file", async () => { const bundlerConfigPath = "some path.js"; testInjector.resolve("fs").exists = (filePath: string) => diff --git a/test/services/bundler/vite-hmr-port-service.ts b/test/services/bundler/vite-hmr-port-service.ts new file mode 100644 index 0000000000..2db0b68f67 --- /dev/null +++ b/test/services/bundler/vite-hmr-port-service.ts @@ -0,0 +1,114 @@ +import { assert } from "chai"; +import * as net from "net"; +import { Yok } from "../../../lib/common/yok"; +import { IInjector } from "../../../lib/common/definitions/yok"; +import { ErrorsStub, LoggerStub } from "../../stubs"; +import { ViteHmrPortServiceImpl } from "../../../lib/services/bundler/vite-hmr-port-service"; + +const ENV_KEYS = ["NS_HMR_PORT", "NS_HMR_STRICT_PORT"] as const; + +function listen(host: string, port = 0): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.once("error", reject); + server.listen(port, host, () => resolve(server)); + }); +} + +function close(server: net.Server): Promise { + return new Promise((resolve) => server.close(() => resolve())); +} + +function portOf(server: net.Server): number { + return (server.address()).port; +} + +function createService(): ViteHmrPortServiceImpl { + const injector: IInjector = new Yok(); + injector.register("errors", ErrorsStub); + injector.register("logger", LoggerStub); + injector.register("viteHmrPortService", ViteHmrPortServiceImpl); + return injector.resolve("viteHmrPortService"); +} + +describe("ViteHmrPortService", () => { + const savedEnv: Partial> = {}; + const holders: net.Server[] = []; + + beforeEach(() => { + for (const key of ENV_KEYS) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(async () => { + for (const key of ENV_KEYS) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + await Promise.all(holders.splice(0).map(close)); + }); + + it("uses the preferred port when it is free", async () => { + // Grab an ephemeral port and release it so it is (almost certainly) + // free for the service to pick. + const probe = await listen("0.0.0.0"); + const free = portOf(probe); + await close(probe); + process.env.NS_HMR_PORT = String(free); + + assert.strictEqual(await createService().getPort("ios"), free); + }); + + it("moves past a port held on the wildcard address", async () => { + const holder = await listen("0.0.0.0"); + holders.push(holder); + process.env.NS_HMR_PORT = String(portOf(holder)); + + const port = await createService().getPort("ios"); + assert.isAbove(port, portOf(holder)); + const server = await listen("0.0.0.0", port); + holders.push(server); + }); + + it("moves past a port that only answers on loopback", async () => { + const holder = await listen("127.0.0.1"); + holders.push(holder); + process.env.NS_HMR_PORT = String(portOf(holder)); + + const port = await createService().getPort("android"); + assert.isAbove(port, portOf(holder)); + }); + + it("resolves the same port for a platform on every call", async () => { + const service = createService(); + const first = await service.getPort("ios"); + assert.strictEqual(await service.getPort("ios"), first); + assert.strictEqual(await service.getPort("iOS"), first); + }); + + it("gives concurrently requested platforms distinct ports", async () => { + const service = createService(); + const [ios, android] = await Promise.all([ + service.getPort("ios"), + service.getPort("android"), + ]); + assert.notStrictEqual(ios, android); + }); + + it("fails instead of moving when NS_HMR_STRICT_PORT is set", async () => { + const holder = await listen("0.0.0.0"); + holders.push(holder); + process.env.NS_HMR_PORT = String(portOf(holder)); + process.env.NS_HMR_STRICT_PORT = "1"; + + await assert.isRejected( + createService().getPort("ios"), + /NS_HMR_STRICT_PORT/, + ); + }); +}); diff --git a/test/services/doctor-service.ts b/test/services/doctor-service.ts index 3c87443ff8..862480c02c 100644 --- a/test/services/doctor-service.ts +++ b/test/services/doctor-service.ts @@ -1,4 +1,4 @@ -import { DoctorService } from "../../lib/services/doctor-service"; +import { DoctorServiceImpl as DoctorService } from "../../lib/services/doctor-service"; import { Yok } from "../../lib/common/yok"; import { LoggerStub, FileSystemStub } from "../stubs"; import { assert } from "chai"; @@ -44,7 +44,7 @@ class DoctorServiceInheritor extends DoctorService { $fs: IFileSystem, $terminalSpinnerService: ITerminalSpinnerService, $versionsService: IVersionsService, - $settingsService: ISettingsService + $settingsService: ISettingsService, ) { super( $analyticsService, @@ -56,13 +56,13 @@ class DoctorServiceInheritor extends DoctorService { $fs, $terminalSpinnerService, $versionsService, - $settingsService + $settingsService, ); } public getDeprecatedShortImportsInFiles( files: string[], - projectDir: string + projectDir: string, ): { file: string; line: string }[] { return super.getDeprecatedShortImportsInFiles(files, projectDir); } @@ -81,10 +81,10 @@ describe("doctorService", () => { testInjector.register("terminalSpinnerService", { execute: ( spinnerOptions: ITerminalSpinnerOptions, - action: () => Promise + action: () => Promise, ): Promise => action(), createSpinner: ( - spinnerOptions?: ITerminalSpinnerOptions + spinnerOptions?: ITerminalSpinnerOptions, ): ITerminalSpinner => { text: "", @@ -99,17 +99,17 @@ describe("doctorService", () => { testInjector.register("jsonFileSettingsService", { getSettingValue: async ( settingName: string, - cacheOpts?: ICacheTimeoutOpts + cacheOpts?: ICacheTimeoutOpts, ): Promise => undefined, saveSetting: async ( key: string, value: any, - cacheOpts?: IUseCacheOpts + cacheOpts?: IUseCacheOpts, ): Promise => undefined, }); testInjector.register("platformEnvironmentRequirements", { checkEnvironmentRequirements: async ( - input: ICheckEnvironmentRequirementsInput + input: ICheckEnvironmentRequirementsInput, ): Promise => {}, }); @@ -343,8 +343,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl { file: "file1", line: 'const application = require("application")' }, { file: "file1", - line: - 'you should import some long words here require("application") module and other words here`)', + line: 'you should import some long words here require("application") module and other words here`)', }, ], }, @@ -352,9 +351,8 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl it("getDeprecatedShortImportsInFiles returns correct results", () => { const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); + const doctorService = + testInjector.resolve("doctorService"); const fs = testInjector.resolve("fs"); fs.getFsStats = (file) => { @@ -372,7 +370,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl const shortImports = doctorService.getDeprecatedShortImportsInFiles( _.keys(filesContents), - "projectDir" + "projectDir", ); assert.deepStrictEqual(shortImports, expectedShortImports); }); @@ -431,9 +429,8 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos"); nsDoctorStub.returns(successGetInfosResult); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); + const doctorService = + testInjector.resolve("doctorService"); const logger = testInjector.resolve("logger"); await doctorService.printWarnings(); assert.isTrue(logger.output.indexOf("No issues were detected.") !== -1); @@ -443,15 +440,14 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos"); nsDoctorStub.returns(failedGetInfosResult); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); + const doctorService = + testInjector.resolve("doctorService"); const logger = testInjector.resolve("logger"); await doctorService.printWarnings(); assert.isTrue( logger.output.indexOf( - "There seem to be issues with your configuration." - ) !== -1 + "There seem to be issues with your configuration.", + ) !== -1, ); }); @@ -459,26 +455,26 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos"); nsDoctorStub.throws( new Error( - "We should not call @nativescript/doctor package when we have results in the file." - ) + "We should not call @nativescript/doctor package when we have results in the file.", + ), ); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); - const jsonFileSettingsService = testInjector.resolve< - IJsonFileSettingsService - >("jsonFileSettingsService"); + const doctorService = + testInjector.resolve("doctorService"); + const jsonFileSettingsService = + testInjector.resolve( + "jsonFileSettingsService", + ); jsonFileSettingsService.getSettingValue = async ( settingName: string, - cacheOpts?: ICacheTimeoutOpts + cacheOpts?: ICacheTimeoutOpts, ): Promise => successGetInfosResult; let saveSettingValue: any = null; jsonFileSettingsService.saveSetting = async ( key: string, value: any, - cacheOpts?: IUseCacheOpts + cacheOpts?: IUseCacheOpts, ): Promise => (saveSettingValue = value); const logger = testInjector.resolve("logger"); await doctorService.printWarnings(); @@ -491,17 +487,17 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl nsDoctorStub.returns(successGetInfosResult); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); - const jsonFileSettingsService = testInjector.resolve< - IJsonFileSettingsService - >("jsonFileSettingsService"); + const doctorService = + testInjector.resolve("doctorService"); + const jsonFileSettingsService = + testInjector.resolve( + "jsonFileSettingsService", + ); let saveSettingValue: any = null; jsonFileSettingsService.saveSetting = async ( key: string, value: any, - cacheOpts?: IUseCacheOpts + cacheOpts?: IUseCacheOpts, ): Promise => (saveSettingValue = value); const logger = testInjector.resolve("logger"); await doctorService.printWarnings(); @@ -514,17 +510,17 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl nsDoctorStub.returns(successGetInfosResult); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); - const jsonFileSettingsService = testInjector.resolve< - IJsonFileSettingsService - >("jsonFileSettingsService"); + const doctorService = + testInjector.resolve("doctorService"); + const jsonFileSettingsService = + testInjector.resolve( + "jsonFileSettingsService", + ); let saveSettingValue: any = null; let isGetSettingValueCalled = false; jsonFileSettingsService.getSettingValue = async ( settingName: string, - cacheOpts?: ICacheTimeoutOpts + cacheOpts?: ICacheTimeoutOpts, ): Promise => { isGetSettingValueCalled = true; return null; @@ -532,7 +528,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl jsonFileSettingsService.saveSetting = async ( key: string, value: any, - cacheOpts?: IUseCacheOpts + cacheOpts?: IUseCacheOpts, ): Promise => (saveSettingValue = value); const logger = testInjector.resolve("logger"); await doctorService.printWarnings({ forceCheck: true }); @@ -541,7 +537,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl assert.isTrue(nsDoctorStub.calledOnce); assert.isFalse( isGetSettingValueCalled, - "When forceCheck is passed, we should not read the cache file." + "When forceCheck is passed, we should not read the cache file.", ); }); @@ -549,9 +545,8 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl const nsDoctorStub = sandbox.stub(nativescriptDoctor.doctor, "getInfos"); nsDoctorStub.returns(failedGetInfosResult); const testInjector = createTestInjector(); - const doctorService = testInjector.resolve( - "doctorService" - ); + const doctorService = + testInjector.resolve("doctorService"); const fs = testInjector.resolve("fs"); let deletedPath = ""; fs.deleteFile = (filePath: string): void => { @@ -561,8 +556,8 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl await doctorService.printWarnings(); assert.isTrue( logger.output.indexOf( - "There seem to be issues with your configuration." - ) !== -1 + "There seem to be issues with your configuration.", + ) !== -1, ); assert.isTrue(deletedPath.indexOf("doctor-cache.json") !== -1); }); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index a23e267988..a6b1970ee5 100644 --- a/test/services/extensibility-service.ts +++ b/test/services/extensibility-service.ts @@ -40,11 +40,11 @@ interface ITestExtensionDefinition { } describe("extensibilityService", () => { - before(() => { + beforeAll(() => { path.resolve = (p: string) => p; }); - after(() => { + afterAll(() => { path.resolve = originalResolve; }); diff --git a/test/services/ios/spm-pbxproj-service.ts b/test/services/ios/spm-pbxproj-service.ts new file mode 100644 index 0000000000..44a1f6a7eb --- /dev/null +++ b/test/services/ios/spm-pbxproj-service.ts @@ -0,0 +1,415 @@ +import { assert } from "chai"; +import { + mkdtempSync, + mkdirSync, + copyFileSync, + readFileSync, + writeFileSync, +} from "fs"; +import { tmpdir } from "os"; +import * as path from "path"; +import { Yok } from "../../../lib/common/yok"; +import { + SPMPbxprojService, + classifyVersion, +} from "../../../lib/services/ios/spm-pbxproj-service"; +import { FileSystem } from "../../../lib/common/file-system"; +import { IInjector } from "../../../lib/common/definitions/yok"; + +// the target that exists in test/files/project.pbxproj +const TARGET_NAME = "TNSBlank"; +// its PBXFrameworksBuildPhase uuid in that fixture (a group is also named +// "Frameworks", so tests that strip the phase must key on the uuid) +const FRAMEWORKS_PHASE_ID = "858B83F418CA22B800AB12DE"; + +const remotePackage: IosSPMPackage = { + name: "swift-numerics", + libs: ["RealModule", "ComplexModule"], + repositoryURL: "https://github.com/apple/swift-numerics.git", + version: "1.0.0", +}; + +const localPackage: IosSPMPackage = { + name: "LocalPkg", + libs: ["LocalPkg"], + path: "vendor/LocalPkg", +}; + +let warnings: string[] = []; + +function createTestInjector(): IInjector { + const injector = new Yok(); + warnings = []; + injector.register("fs", FileSystem); + injector.register("logger", { + warn: (message: string) => warnings.push(message), + trace: (): void => undefined, + debug: (): void => undefined, + }); + injector.register("xcode", require("nativescript-dev-xcode")); + injector.register("spmPbxprojService", SPMPbxprojService); + return injector; +} + +/** Creates a platform project root containing a copy of the fixture pbxproj. */ +function createProjectRoot(): string { + const projectRoot = mkdtempSync(path.join(tmpdir(), "spm-pbxproj-")); + const xcodeprojPath = path.join(projectRoot, `${TARGET_NAME}.xcodeproj`); + mkdirSync(xcodeprojPath); + copyFileSync( + path.join(__dirname, "..", "..", "files", "project.pbxproj"), + path.join(xcodeprojPath, "project.pbxproj"), + ); + return projectRoot; +} + +function readPbxproj(projectRoot: string): string { + return readFileSync( + path.join(projectRoot, `${TARGET_NAME}.xcodeproj`, "project.pbxproj"), + "utf8", + ); +} + +function countOccurrences(contents: string, needle: string): number { + return contents.split(needle).length - 1; +} + +describe("SPMPbxprojService", () => { + let service: ISPMPbxprojService; + let projectRoot: string; + + beforeEach(() => { + service = createTestInjector().resolve("spmPbxprojService"); + projectRoot = createProjectRoot(); + }); + + describe("classifyVersion", () => { + const testCases: Array<{ + version: string; + expected: Record; + }> = [ + { + version: "1.0.0", + expected: { kind: "exactVersion", version: "1.0.0" }, + }, + { + version: "^2.5.0", + expected: { kind: "upToNextMajorVersion", minimumVersion: "2.5.0" }, + }, + { + version: "~3.1.0", + expected: { kind: "upToNextMinorVersion", minimumVersion: "3.1.0" }, + }, + { + version: ">=1.2.0 <2.0.0", + expected: { + kind: "versionRange", + minimumVersion: "1.2.0", + maximumVersion: "2.0.0", + }, + }, + { version: "main", expected: { kind: "branch", branch: "main" } }, + { + version: "#5f03bfdc8cb6300ef8355695a3d27d11ba19f6a3", + expected: { + kind: "revision", + revision: "5f03bfdc8cb6300ef8355695a3d27d11ba19f6a3", + }, + }, + ]; + + testCases.forEach(({ version, expected }) => { + it(`maps "${version}" to ${expected.kind}`, () => { + assert.deepEqual(classifyVersion(version), expected); + }); + }); + }); + + describe("addPackages", () => { + it("writes a remote package reference and links each of its libs", () => { + const result = service.addPackages(projectRoot, [ + { targetName: TARGET_NAME, package: remotePackage }, + ]); + + assert.isTrue(result); + const contents = readPbxproj(projectRoot); + + // the package reference itself, listed on the project + assert.include( + contents, + 'XCRemoteSwiftPackageReference "swift-numerics"', + "expected a remote package reference section entry", + ); + assert.include(contents, "repositoryURL = "); + assert.include(contents, "https://github.com/apple/swift-numerics.git"); + assert.include(contents, "kind = exactVersion"); + assert.include(contents, "packageReferences = ("); + + // one product dependency + build file + Frameworks entry per lib + for (const lib of remotePackage.libs) { + assert.include( + contents, + `productName = ${lib}`, + `expected a product dependency for ${lib}`, + ); + assert.include( + contents, + `${lib} in Frameworks`, + `expected ${lib} in the Frameworks build phase`, + ); + } + assert.include(contents, "packageProductDependencies = ("); + }); + + it("writes a local package reference relative to the project root", () => { + const absolutePackagePath = path.join(projectRoot, "vendor", "LocalPkg"); + const result = service.addPackages(projectRoot, [ + { + targetName: TARGET_NAME, + package: { ...localPackage, path: absolutePackagePath }, + }, + ]); + + assert.isTrue(result); + const contents = readPbxproj(projectRoot); + + assert.include( + contents, + 'XCLocalSwiftPackageReference "vendor/LocalPkg"', + "expected the local package to be recorded by relative path", + ); + assert.include(contents, "relativePath = "); + assert.notInclude( + contents, + absolutePackagePath, + "the absolute path must not leak into the pbxproj", + ); + }); + + it("is idempotent — reapplying the same packages does not duplicate entries", () => { + const assignments: IosSPMPackageAssignment[] = [ + { targetName: TARGET_NAME, package: remotePackage }, + ]; + + assert.isTrue(service.addPackages(projectRoot, assignments)); + const afterFirst = readPbxproj(projectRoot); + + assert.isTrue(service.addPackages(projectRoot, assignments)); + const afterSecond = readPbxproj(projectRoot); + + assert.equal( + afterSecond, + afterFirst, + "reapplying the same packages should leave the pbxproj byte-identical", + ); + assert.equal( + countOccurrences( + afterSecond, + 'XCRemoteSwiftPackageReference "swift-numerics" */ = {', + ), + 1, + "the package reference should be defined exactly once", + ); + assert.equal( + countOccurrences(afterSecond, "RealModule in Frameworks */ = {"), + 1, + "the build file should be defined exactly once", + ); + }); + + it("updates an existing package reference in place when the version changes", () => { + assert.isTrue( + service.addPackages(projectRoot, [ + { targetName: TARGET_NAME, package: remotePackage }, + ]), + ); + assert.isTrue( + service.addPackages(projectRoot, [ + { + targetName: TARGET_NAME, + package: { ...remotePackage, version: "2.0.0" }, + }, + ]), + ); + + const contents = readPbxproj(projectRoot); + assert.equal( + countOccurrences( + contents, + 'XCRemoteSwiftPackageReference "swift-numerics" */ = {', + ), + 1, + "the package should still be defined exactly once", + ); + assert.include(contents, "version = 2.0.0"); + assert.notInclude(contents, "version = 1.0.0"); + }); + + it("skips a target without a Frameworks build phase, warns, and writes nothing", () => { + // strip the Frameworks build phase from the fixture target — both the + // section entry and its slot in the target's buildPhases + const pbxPath = path.join( + projectRoot, + `${TARGET_NAME}.xcodeproj`, + "project.pbxproj", + ); + const stripped = readFileSync(pbxPath, "utf8") + .replace( + new RegExp( + `^\\s*${FRAMEWORKS_PHASE_ID} /\\* Frameworks \\*/,\\n`, + "m", + ), + "", + ) + .replace( + new RegExp( + `^\\s*${FRAMEWORKS_PHASE_ID} /\\* Frameworks \\*/ = \\{[\\s\\S]*?\\};\\n`, + "m", + ), + "", + ); + writeFileSync(pbxPath, stripped); + + const result = service.addPackages(projectRoot, [ + { targetName: TARGET_NAME, package: remotePackage }, + ]); + + assert.isFalse( + result, + "nothing could be applied, so nothing was written", + ); + assert.isTrue( + warnings.some((w) => w.includes("no Frameworks build phase")), + `expected a warning about the missing build phase, got: ${warnings}`, + ); + const contents = readPbxproj(projectRoot); + assert.notInclude(contents, "XCRemoteSwiftPackageReference"); + assert.notInclude( + contents, + "packageReferences", + "the skipped package must leave no trace, not even an empty list", + ); + }); + + it("keeps same-named products from different packages distinct", () => { + const otherPackage: IosSPMPackage = { + name: "other-numerics", + libs: ["RealModule"], + repositoryURL: "https://example.com/other/other-numerics.git", + version: "2.0.0", + }; + const assignments: IosSPMPackageAssignment[] = [ + { targetName: TARGET_NAME, package: remotePackage }, + { targetName: TARGET_NAME, package: otherPackage }, + ]; + + assert.isTrue(service.addPackages(projectRoot, assignments)); + // reapply to prove the scoped lookup is still idempotent + assert.isTrue(service.addPackages(projectRoot, assignments)); + + const xcode = require("nativescript-dev-xcode"); + const project = new xcode.project( + path.join(projectRoot, `${TARGET_NAME}.xcodeproj`, "project.pbxproj"), + ); + project.parseSync(); + const section = + project.hash.project.objects["XCSwiftPackageProductDependency"]; + const realModuleDeps = Object.keys(section) + .filter((key) => !key.endsWith("_comment")) + .map((key) => section[key]) + .filter((entry) => entry.productName === "RealModule"); + + assert.equal( + realModuleDeps.length, + 2, + "each package should own its own RealModule product dependency", + ); + assert.equal( + new Set(realModuleDeps.map((entry) => entry.package)).size, + 2, + "the two product dependencies should point at different packages", + ); + }); + + it("quotes requirement values a pbxproj cannot hold bare", () => { + assert.isTrue( + service.addPackages(projectRoot, [ + { + targetName: TARGET_NAME, + package: { ...remotePackage, version: "1.0.0-beta.1" }, + }, + ]), + ); + + assert.include(readPbxproj(projectRoot), 'version = "1.0.0-beta.1";'); + }); + + it("quotes branch requirements containing spaces", () => { + assert.isTrue( + service.addPackages(projectRoot, [ + { + targetName: TARGET_NAME, + package: { ...remotePackage, version: "release 1.0" }, + }, + ]), + ); + + assert.include(readPbxproj(projectRoot), 'branch = "release 1.0";'); + }); + + it("skips a package whose target is missing, and warns", () => { + const result = service.addPackages(projectRoot, [ + { targetName: "NoSuchTarget", package: remotePackage }, + ]); + + assert.isFalse( + result, + "nothing could be applied, so nothing was written", + ); + assert.isTrue( + warnings.some((w) => w.includes("NoSuchTarget")), + `expected a warning naming the missing target, got: ${warnings}`, + ); + assert.notInclude( + readPbxproj(projectRoot), + "XCRemoteSwiftPackageReference", + ); + }); + + it("still applies packages for targets that do exist when another is missing", () => { + const result = service.addPackages(projectRoot, [ + { targetName: "NoSuchTarget", package: localPackage }, + { targetName: TARGET_NAME, package: remotePackage }, + ]); + + assert.isTrue(result); + const contents = readPbxproj(projectRoot); + assert.include( + contents, + 'XCRemoteSwiftPackageReference "swift-numerics"', + ); + assert.notInclude(contents, "XCLocalSwiftPackageReference"); + }); + + it("returns false when there are no packages to apply", () => { + assert.isFalse(service.addPackages(projectRoot, [])); + }); + + it("returns false when the project root has no .xcodeproj", () => { + const emptyRoot = mkdtempSync(path.join(tmpdir(), "spm-empty-")); + assert.isFalse( + service.addPackages(emptyRoot, [ + { targetName: TARGET_NAME, package: remotePackage }, + ]), + ); + }); + + it("returns false when the project root does not exist", () => { + assert.isFalse( + service.addPackages(path.join(tmpdir(), "spm-does-not-exist"), [ + { targetName: TARGET_NAME, package: remotePackage }, + ]), + ); + }); + }); +}); diff --git a/test/services/ios/xcodebuild-args-service.ts b/test/services/ios/xcodebuild-args-service.ts index 66fc46f482..6bffa9e9e0 100644 --- a/test/services/ios/xcodebuild-args-service.ts +++ b/test/services/ios/xcodebuild-args-service.ts @@ -11,6 +11,7 @@ function createTestInjector(data: { logLevel: string; hasProjectWorkspace: boolean; connectedDevices?: any[]; + buildXcconfigContent?: string; }): IInjector { const injector = new Yok(); injector.register("devicePlatformsConstants", DevicePlatformsConstants); @@ -19,8 +20,11 @@ function createTestInjector(data: { getDevicesForPlatform: () => data.connectedDevices || [], }); injector.register("fs", { - exists: () => data.hasProjectWorkspace, - readText: () => "", + exists: (filePath: string) => + filePath.endsWith("build.xcconfig") + ? data.buildXcconfigContent !== undefined + : data.hasProjectWorkspace, + readText: () => data.buildXcconfigContent || "", }); injector.register("logger", { getLevel: () => data.logLevel, @@ -49,7 +53,13 @@ function getCommonArgs() { } function getXcodeProjectArgs(data?: { hasProjectWorkspace: boolean }) { - const extraArgs = ["-scheme", projectName, "-skipPackagePluginValidation"]; + const extraArgs = [ + "-scheme", + projectName, + "-skipPackagePluginValidation", + "-skipMacroValidation", + "SWIFT_ENABLE_EXPLICIT_MODULES=NO", + ]; return data && data.hasProjectWorkspace ? [ "-workspace", @@ -72,6 +82,115 @@ function getBuildLoggingArgs(logLevel: string): string[] { } describe("xcodebuildArgsService", () => { + describe("getXcodeProjectArgs", () => { + const originalAuthProvider = process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER; + + afterEach(() => { + if (originalAuthProvider === undefined) { + delete process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER; + } else { + process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER = originalAuthProvider; + } + }); + + it("should allow SWIFT_ENABLE_EXPLICIT_MODULES to be overridden from build.xcconfig", () => { + const injector = createTestInjector({ + logLevel: "INFO", + hasProjectWorkspace: false, + buildXcconfigContent: "SWIFT_ENABLE_EXPLICIT_MODULES = YES", + }); + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve( + "xcodebuildArgsService", + ); + + const actualArgs = xcodebuildArgsService.getXcodeProjectArgs( + { projectRoot, normalizedPlatformName }, + { projectName, appResourcesDirectoryPath }, + ); + + assert.include(actualArgs, "SWIFT_ENABLE_EXPLICIT_MODULES=YES"); + assert.notInclude(actualArgs, "SWIFT_ENABLE_EXPLICIT_MODULES=NO"); + }); + + it("should include DEVELOPMENT_TEAM from build.xcconfig", () => { + const injector = createTestInjector({ + logLevel: "INFO", + hasProjectWorkspace: false, + buildXcconfigContent: "DEVELOPMENT_TEAM = TEAM123", + }); + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve( + "xcodebuildArgsService", + ); + + const actualArgs = xcodebuildArgsService.getXcodeProjectArgs( + { projectRoot, normalizedPlatformName }, + { projectName, appResourcesDirectoryPath }, + ); + + assert.include(actualArgs, "DEVELOPMENT_TEAM=TEAM123"); + }); + + it("passes the package authorization provider from the environment", () => { + process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER = "netrc"; + const injector = createTestInjector({ + logLevel: "INFO", + hasProjectWorkspace: false, + }); + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve( + "xcodebuildArgsService", + ); + + const actualArgs = xcodebuildArgsService.getXcodeProjectArgs( + { projectRoot, normalizedPlatformName }, + { projectName, appResourcesDirectoryPath }, + ); + + const index = actualArgs.indexOf("-packageAuthorizationProvider"); + assert.notStrictEqual(index, -1); + assert.strictEqual(actualArgs[index + 1], "netrc"); + }); + + it("omits the package authorization provider when unset", () => { + delete process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER; + const injector = createTestInjector({ + logLevel: "INFO", + hasProjectWorkspace: false, + }); + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve( + "xcodebuildArgsService", + ); + + const actualArgs = xcodebuildArgsService.getXcodeProjectArgs( + { projectRoot, normalizedPlatformName }, + { projectName, appResourcesDirectoryPath }, + ); + + assert.notInclude(actualArgs, "-packageAuthorizationProvider"); + }); + + it("keeps the project path as the second argument", () => { + process.env.NS_PACKAGE_AUTHORIZATION_PROVIDER = "keychain"; + const injector = createTestInjector({ + logLevel: "INFO", + hasProjectWorkspace: false, + }); + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve( + "xcodebuildArgsService", + ); + + const actualArgs = xcodebuildArgsService.getXcodeProjectArgs( + { projectRoot, normalizedPlatformName }, + { projectName, appResourcesDirectoryPath }, + ); + + assert.strictEqual(actualArgs[0], "-project"); + assert.strictEqual( + actualArgs[1], + path.join(projectRoot, `${projectName}.xcodeproj`), + ); + }); + }); + describe("getBuildForSimulatorArgs", () => { _.each([true, false], (hasProjectWorkspace) => { _.each(["INFO", "TRACE"], (logLevel) => { diff --git a/test/services/project-cleanup-service.ts b/test/services/project-cleanup-service.ts new file mode 100644 index 0000000000..08dbf9aa82 --- /dev/null +++ b/test/services/project-cleanup-service.ts @@ -0,0 +1,78 @@ +import { assert } from "chai"; +import * as path from "path"; +import { Yok } from "../../lib/common/yok"; +import { ProjectCleanupService } from "../../lib/services/project-cleanup-service"; +import { IInjector } from "../../lib/common/definitions/yok"; + +const projectDir = path.join("/tmp", "nsm-cleanup-project"); + +function createTestInjector(deletedPaths: string[]): IInjector { + const testInjector = new Yok(); + testInjector.register("fs", { + exists: (p: string) => !deletedPaths.includes(p), + getFsStats: () => ({ isDirectory: () => true }), + getSize: () => 0, + deleteDirectorySafe: (p: string) => deletedPaths.push(p), + deleteFile: (p: string) => deletedPaths.push(p), + }); + testInjector.register("logger", { + trace: (): void => undefined, + warn: (): void => undefined, + info: (): void => undefined, + }); + testInjector.register("projectHelper", { projectDir }); + testInjector.register("terminalSpinnerService", { + createSpinner: () => ({ + clear: (): void => undefined, + start: (): void => undefined, + stop: (): void => undefined, + succeed: (): void => undefined, + fail: (): void => undefined, + text: "", + }), + }); + + return testInjector; +} + +describe("projectCleanupService", () => { + let deletedPaths: string[]; + let service: ProjectCleanupService; + + beforeEach(() => { + deletedPaths = []; + service = createTestInjector(deletedPaths).resolve(ProjectCleanupService); + }); + + it("cleans a path inside the project", async () => { + const result = await service.clean(["platforms"], { silent: true }); + + assert.isTrue(result.ok); + assert.deepStrictEqual(deletedPaths, [path.join(projectDir, "platforms")]); + }); + + it("refuses a path that escapes the project directory", async () => { + const result = await service.clean(["../sibling"], { silent: true }); + + assert.isFalse(result.ok); + assert.deepStrictEqual(deletedPaths, []); + }); + + it("refuses an absolute path outside the project directory", async () => { + const result = await service.clean([path.join("/tmp", "elsewhere")], { + silent: true, + }); + + assert.isFalse(result.ok); + assert.deepStrictEqual(deletedPaths, []); + }); + + it("allows an absolute path that resolves inside the project", async () => { + const result = await service.clean([path.join(projectDir, "platforms")], { + silent: true, + }); + + assert.isTrue(result.ok); + assert.deepStrictEqual(deletedPaths, [path.join(projectDir, "platforms")]); + }); +}); diff --git a/test/services/project-config-service.ts b/test/services/project-config-service.ts index d4e3d09b19..72a750f85a 100644 --- a/test/services/project-config-service.ts +++ b/test/services/project-config-service.ts @@ -3,7 +3,9 @@ import { assert } from "chai"; import * as _ from "lodash"; import { LoggerStub, ProjectHelperStub, ErrorsStub } from "../stubs"; import { CONFIG_FILE_NAME_JS, CONFIG_FILE_NAME_TS } from "../../lib/constants"; -import { basename } from "path"; +import { basename, join } from "path"; +import * as os from "os"; +import * as fs from "fs"; import { IInjector } from "../../lib/common/definitions/yok"; import { IReadFileOptions, IFsStats } from "../../lib/common/declarations"; import { ProjectConfigService } from "../../lib/services/project-config-service"; @@ -13,7 +15,8 @@ import { SettingsService } from "../../lib/common/test/unit-tests/stubs"; const createTestInjector = ( readTextCallback: (filename: string) => string, - existsCallback: (filePath: string) => boolean + existsCallback: (filePath: string) => boolean, + projectDir: string = "/my/project", ): IInjector => { const testInjector = new Yok(); @@ -22,21 +25,21 @@ const createTestInjector = ( testInjector.register("options", Options); testInjector.register( "projectHelper", - new ProjectHelperStub(null, "/my/project") + new ProjectHelperStub(null, projectDir), ); testInjector.register("fs", { writeJson: ( filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { /** intentionally left blank */ }, readText: ( filename: string, - encoding?: IReadFileOptions | string + encoding?: IReadFileOptions | string, ): string => { return readTextCallback(filename); }, @@ -54,7 +57,7 @@ const createTestInjector = ( enumerateDirectories?: boolean; includeEmptyDirectories?: boolean; }, - foundFiles?: string[] + foundFiles?: string[], ): string[] => [], }); testInjector.register("logger", LoggerStub); @@ -94,10 +97,10 @@ describe("projectConfigService", () => { it("works with JS config", () => { const testInjector = createTestInjector( (filename) => sampleJSConfig, - (filePath) => basename(filePath) === CONFIG_FILE_NAME_JS + (filePath) => basename(filePath) === CONFIG_FILE_NAME_JS, ); const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -107,10 +110,10 @@ describe("projectConfigService", () => { it("JS config parse deep key path", () => { const testInjector = createTestInjector( (filename) => sampleJSConfig, - (filePath) => basename(filePath) === CONFIG_FILE_NAME_JS + (filePath) => basename(filePath) === CONFIG_FILE_NAME_JS, ); const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("android.v8Flags"); @@ -120,10 +123,10 @@ describe("projectConfigService", () => { it("works with TS config", () => { const testInjector = createTestInjector( (filename) => sampleTSConfig, - (filePath) => basename(filePath) === CONFIG_FILE_NAME_TS + (filePath) => basename(filePath) === CONFIG_FILE_NAME_TS, ); const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -133,10 +136,10 @@ describe("projectConfigService", () => { it("TS config parse deep key path", () => { const testInjector = createTestInjector( (filename) => sampleTSConfig, - (filePath) => basename(filePath) === CONFIG_FILE_NAME_TS + (filePath) => basename(filePath) === CONFIG_FILE_NAME_TS, ); const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("android.v8Flags"); @@ -146,7 +149,7 @@ describe("projectConfigService", () => { it("can read a named JS config file when passing --config", async () => { const testInjector = createTestInjector( (filename) => sampleJSConfig, - (filePath) => basename(filePath) === "custom.config.js" + (filePath) => basename(filePath) === "custom.config.js", ); // mock "--config custom.config.js" @@ -155,7 +158,7 @@ describe("projectConfigService", () => { options.config = "custom.config.js"; const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -165,7 +168,7 @@ describe("projectConfigService", () => { it("can read a named TS config file when passing --config", async () => { const testInjector = createTestInjector( (filename) => sampleTSConfig, - (filePath) => basename(filePath) === "custom.config.ts" + (filePath) => basename(filePath) === "custom.config.ts", ); // mock "--config custom.config.ts" @@ -174,7 +177,7 @@ describe("projectConfigService", () => { options.config = "custom.config.ts"; const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -184,7 +187,7 @@ describe("projectConfigService", () => { it("can read a named JS config file when passing --config without extension", async () => { const testInjector = createTestInjector( (filename) => sampleJSConfig, - (filePath) => basename(filePath) === "custom.config.js" + (filePath) => basename(filePath) === "custom.config.js", ); // mock "--config custom.config" @@ -193,7 +196,7 @@ describe("projectConfigService", () => { options.config = "custom.config"; const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -203,7 +206,7 @@ describe("projectConfigService", () => { it("can read a named TS config file when passing --config without extension", async () => { const testInjector = createTestInjector( (filename) => sampleTSConfig, - (filePath) => basename(filePath) === "custom.config.ts" + (filePath) => basename(filePath) === "custom.config.ts", ); // mock "--config custom.config" @@ -212,7 +215,7 @@ describe("projectConfigService", () => { options.config = "custom.config"; const projectConfigService: IProjectConfigService = testInjector.resolve( - "projectConfigService" + "projectConfigService", ); const actualValue = projectConfigService.getValue("id"); @@ -249,4 +252,129 @@ describe("projectConfigService", () => { // ); // }); }); + + describe("setValue", () => { + const tempProjectDirs: string[] = []; + + const createProjectDir = (prettierSource?: string): string => { + const projectDir = fs.mkdtempSync( + join(os.tmpdir(), "ns-config-service-"), + ); + tempProjectDirs.push(projectDir); + + if (prettierSource) { + const prettierDir = join(projectDir, "node_modules", "prettier"); + fs.mkdirSync(prettierDir, { recursive: true }); + fs.writeFileSync( + join(prettierDir, "package.json"), + JSON.stringify({ name: "prettier", main: "index.js" }), + ); + fs.writeFileSync(join(prettierDir, "index.js"), prettierSource); + } + + return projectDir; + }; + + const setup = (projectDir: string) => { + let content = sampleTSConfig; + const writes: string[] = []; + const testInjector = createTestInjector( + () => content, + (filePath) => basename(filePath) === CONFIG_FILE_NAME_TS, + projectDir, + ); + const fsStub: any = testInjector.resolve("fs"); + fsStub.writeFile = (filePath: string, data: string) => { + writes.push(data); + content = data; + }; + + return { + writes, + logger: testInjector.resolve("logger"), + projectConfigService: testInjector.resolve( + "projectConfigService", + ), + }; + }; + + afterEach(() => { + while (tempProjectDirs.length) { + fs.rmSync(tempProjectDirs.pop(), { recursive: true, force: true }); + } + }); + + it("formats with the prettier installed in the project", async () => { + const { writes, projectConfigService } = setup( + createProjectDir(`module.exports = { + resolveConfig: () => Promise.resolve(null), + format: (source) => "// project prettier\\n" + source, + };`), + ); + + const result = await projectConfigService.setValue( + "id", + "io.test.updated", + ); + + assert.isTrue(result); + assert.equal(writes.length, 1); + assert.include(writes[0], "// project prettier"); + assert.include(writes[0], "io.test.updated"); + }); + + it("awaits the project prettier when its format is async", async () => { + const { writes, projectConfigService } = setup( + createProjectDir(`module.exports = { + resolveConfig: () => Promise.resolve(null), + format: async (source) => "// project prettier\\n" + source, + };`), + ); + + const result = await projectConfigService.setValue( + "id", + "io.test.updated", + ); + + assert.isTrue(result); + assert.include(writes[0], "// project prettier"); + assert.include(writes[0], "io.test.updated"); + }); + + it("writes the unformatted config when prettier fails", async () => { + const { writes, logger, projectConfigService } = setup( + createProjectDir(`module.exports = { + resolveConfig: () => Promise.resolve(null), + format: () => { + throw new Error("prettier is broken"); + }, + };`), + ); + + const result = await projectConfigService.setValue( + "id", + "io.test.updated", + ); + + assert.isTrue(result); + assert.equal(writes.length, 1); + assert.include(writes[0], "io.test.updated"); + assert.include(logger.warnOutput, "Could not format the config"); + }); + + it("falls back to the bundled prettier when the project has none", async () => { + const { writes, logger, projectConfigService } = + setup(createProjectDir()); + + const result = await projectConfigService.setValue( + "id", + "io.test.updated", + ); + + assert.isTrue(result); + assert.equal(writes.length, 1); + assert.include(writes[0], "io.test.updated"); + assert.notInclude(logger.warnOutput, "Could not format the config"); + }); + }); }); diff --git a/test/services/project-data-service.ts b/test/services/project-data-service.ts index 401ebaadda..753672f3b4 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -63,7 +63,7 @@ const testData: any = [ const createTestInjector = ( packageJsonContent?: string, - nsConfigContent?: string + nsConfigContent?: string, ): IInjector => { const testInjector = new Yok(); testInjector.register("projectData", ProjectDataStub); @@ -77,7 +77,7 @@ const createTestInjector = ( filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { /** intentionally left blank */ }, @@ -86,7 +86,7 @@ const createTestInjector = ( readText: ( filename: string, - encoding?: IReadFileOptions | string + encoding?: IReadFileOptions | string, ): string => { if (filename.indexOf("package.json") > -1) { return packageJsonContent; @@ -111,7 +111,7 @@ const createTestInjector = ( enumerateDirectories?: boolean; includeEmptyDirectories?: boolean; }, - foundFiles?: string[] + foundFiles?: string[], ): string[] => [], }); @@ -143,10 +143,10 @@ const createTestInjector = ( describe("projectDataService", () => { const generateJsonDataFromTestData = ( currentTestData: any, - skipNativeScriptKey?: boolean + skipNativeScriptKey?: boolean, ) => { const props = currentTestData.propertyName.split( - NATIVESCRIPT_PROPS_INTERNAL_DELIMITER + NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, ); const data: any = {}; let currentData: any = skipNativeScriptKey @@ -168,28 +168,28 @@ describe("projectDataService", () => { const generateFileContentFromTestData = ( currentTestData: any, - skipNativeScriptKey?: boolean + skipNativeScriptKey?: boolean, ) => { const data = generateJsonDataFromTestData( currentTestData, - skipNativeScriptKey + skipNativeScriptKey, ); return JSON.stringify(data); }; - describe("getNSValue", () => { + // every entry in testData is commented out, so this generates no cases + describe.skip("getNSValue", () => { _.each(testData, (currentTestData) => { it(currentTestData.description, () => { const testInjector = createTestInjector( - generateFileContentFromTestData(currentTestData) - ); - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" + generateFileContentFromTestData(currentTestData), ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); const actualValue = projectDataService.getNSValue( "projectDir", - currentTestData.propertyName + currentTestData.propertyName, ); assert.deepStrictEqual(actualValue, currentTestData.propertyValue); }); @@ -203,7 +203,7 @@ describe("projectDataService", () => { defaultEmptyData[CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}; const testInjector = createTestInjector( - JSON.stringify(defaultEmptyData) + JSON.stringify(defaultEmptyData), ); const fs: IFileSystem = testInjector.resolve("fs"); @@ -212,27 +212,26 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.setNSValue( "projectDir", currentTestData.propertyName, - currentTestData.propertyValue + currentTestData.propertyValue, ); assert.deepStrictEqual( dataPassedToWriteJson, - generateJsonDataFromTestData(currentTestData) + generateJsonDataFromTestData(currentTestData), ); assert.isTrue( !!dataPassedToWriteJson[CLIENT_NAME_KEY_IN_PROJECT_FILE], - "Data passed to write JSON must contain nativescript key." + "Data passed to write JSON must contain nativescript key.", ); }); }); @@ -254,34 +253,33 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.setNSValue( "projectDir", getPropertyName(["root", "id"]), - "2" + "2", ); const expectedData = _.cloneDeep(initialData); expectedData[CLIENT_NAME_KEY_IN_PROJECT_FILE].root.id = "2"; assert.isTrue( !!dataPassedToWriteJson[CLIENT_NAME_KEY_IN_PROJECT_FILE], - "Data passed to write JSON must contain nativescript key." + "Data passed to write JSON must contain nativescript key.", ); assert.deepStrictEqual(dataPassedToWriteJson, expectedData); assert.deepStrictEqual( dataPassedToWriteJson[CLIENT_NAME_KEY_IN_PROJECT_FILE].root.id, - "2" + "2", ); assert.deepStrictEqual( dataPassedToWriteJson[CLIENT_NAME_KEY_IN_PROJECT_FILE].root .constantItem, - "myValue" + "myValue", ); }); }); @@ -289,7 +287,7 @@ describe("projectDataService", () => { describe("removeNSProperty", () => { const generateExpectedDataFromTestData = (currentTestData: any) => { const props = currentTestData.propertyName.split( - NATIVESCRIPT_PROPS_INTERNAL_DELIMITER + NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, ); props.splice(props.length - 1, 1); @@ -308,7 +306,7 @@ describe("projectDataService", () => { generateFileContentFromTestData(currentTestData); const testInjector = createTestInjector( - generateFileContentFromTestData(currentTestData) + generateFileContentFromTestData(currentTestData), ); const fs: IFileSystem = testInjector.resolve("fs"); @@ -317,26 +315,25 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.removeNSProperty( "projectDir", - currentTestData.propertyName + currentTestData.propertyName, ); assert.deepStrictEqual( dataPassedToWriteJson, - generateExpectedDataFromTestData(currentTestData) + generateExpectedDataFromTestData(currentTestData), ); assert.isTrue( !!dataPassedToWriteJson[CLIENT_NAME_KEY_IN_PROJECT_FILE], - "Data passed to write JSON must contain nativescript key." + "Data passed to write JSON must contain nativescript key.", ); }); }); @@ -358,17 +355,16 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.removeNSProperty( "projectDir", - getPropertyName(["root", "id"]) + getPropertyName(["root", "id"]), ); assert.deepStrictEqual(dataPassedToWriteJson, { nativescript: { root: { constantItem: "myValue" } }, @@ -379,7 +375,7 @@ describe("projectDataService", () => { describe("removeNSConfigProperty", () => { const generateExpectedDataFromTestData = (currentTestData: any) => { const props = currentTestData.propertyName.split( - NATIVESCRIPT_PROPS_INTERNAL_DELIMITER + NATIVESCRIPT_PROPS_INTERNAL_DELIMITER, ); props.splice(props.length - 1, 1); @@ -397,7 +393,7 @@ describe("projectDataService", () => { it(currentTestData.description, () => { const testInjector = createTestInjector( null, - generateFileContentFromTestData(currentTestData, true) + generateFileContentFromTestData(currentTestData, true), ); const fs: IFileSystem = testInjector.resolve("fs"); @@ -406,30 +402,29 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); const propDelimiterRegExp = new RegExp( regExpEscape(NATIVESCRIPT_PROPS_INTERNAL_DELIMITER), - "g" + "g", ); const propertySelector = currentTestData.propertyName.replace( propDelimiterRegExp, - "." + ".", ); projectDataService.removeNSConfigProperty( "projectDir", - propertySelector + propertySelector, ); assert.deepStrictEqual( dataPassedToWriteJson, - generateExpectedDataFromTestData(currentTestData) + generateExpectedDataFromTestData(currentTestData), ); }); }); @@ -451,17 +446,16 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.removeNSProperty( "projectDir", - getPropertyName(["root", "id"]) + getPropertyName(["root", "id"]), ); assert.deepStrictEqual(dataPassedToWriteJson, { nativescript: { root: { constantItem: "myValue" } }, @@ -477,7 +471,7 @@ describe("projectDataService", () => { }; const testInjector = createTestInjector( - generateFileContentFromTestData(currentTestData, true) + generateFileContentFromTestData(currentTestData, true), ); const fs: IFileSystem = testInjector.resolve("fs"); @@ -486,14 +480,13 @@ describe("projectDataService", () => { filename: string, data: any, space?: string, - encoding?: string + encoding?: string, ): void => { dataPassedToWriteJson = data; }; - const projectDataService: IProjectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService: IProjectDataService = + testInjector.resolve("projectDataService"); projectDataService.removeDependency("projectDir", "myDeps"); assert.deepStrictEqual(dataPassedToWriteJson, { dependencies: {} }); @@ -514,9 +507,8 @@ describe("projectDataService", () => { throw new Error(`Unable to read file ${filePath}`); }; - const projectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService = + testInjector.resolve("projectDataService"); const assetStructure = await projectDataService.getAssetsStructure({ projectDir: ".", }); @@ -590,9 +582,8 @@ describe("projectDataService", () => { } }; - const projectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService = + testInjector.resolve("projectDataService"); const assetStructure = await projectDataService.getAssetsStructure({ projectDir: ".", }); @@ -705,7 +696,7 @@ describe("projectDataService", () => { ]; const setupTestCase = ( - testCase: any + testCase: any, ): { projectDataService: IProjectDataService; testInjector: IInjector } => { const testInjector = createTestInjector(); const fs = testInjector.resolve("fs"); @@ -741,9 +732,8 @@ describe("projectDataService", () => { return []; }; - const projectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService = + testInjector.resolve("projectDataService"); projectDataService.getProjectData = () => { appDirectoryPath, @@ -757,9 +747,8 @@ describe("projectDataService", () => { getAppExecutableFilesTestData.forEach((testCase) => { it(`returns correct files for application type ${testCase.projectType}`, () => { const { projectDataService } = setupTestCase(testCase); - const appExecutableFiles = projectDataService.getAppExecutableFiles( - "projectDir" - ); + const appExecutableFiles = + projectDataService.getAppExecutableFiles("projectDir"); assert.deepStrictEqual(appExecutableFiles, testCase.expectedResult); }); }); @@ -810,9 +799,8 @@ describe("projectDataService", () => { return baseFsGetFsStats(filePath); }; - const appExecutableFiles = projectDataService.getAppExecutableFiles( - "projectDir" - ); + const appExecutableFiles = + projectDataService.getAppExecutableFiles("projectDir"); assert.deepStrictEqual(appExecutableFiles, testCase.expectedResult); }); }); diff --git a/test/spm-service.ts b/test/spm-service.ts index b0ccfeff19..e5170edcde 100644 --- a/test/spm-service.ts +++ b/test/spm-service.ts @@ -1,4 +1,6 @@ import { assert } from "chai"; +import { EOL } from "os"; +import { SPMService } from "../lib/services/ios/spm-service"; /** * Helper function to merge app and plugin SPM packages. @@ -6,14 +8,14 @@ import { assert } from "chai"; */ function mergeSPMPackages(appPackages: any[], pluginPackages: any[]): any[] { const spmPackages = [...appPackages]; - const appPackageNames = new Set(spmPackages.map(pkg => pkg.name)); - + const appPackageNames = new Set(spmPackages.map((pkg) => pkg.name)); + for (const pluginPkg of pluginPackages) { if (!appPackageNames.has(pluginPkg.name)) { spmPackages.push(pluginPkg); } } - + return spmPackages; } @@ -50,7 +52,9 @@ describe("SPM Service - Package Override Logic", () => { // Verify the result assert.equal(spmPackages.length, 2, "Should have 2 packages total"); - const firebasePackage = spmPackages.find((pkg) => pkg.name === "FirebaseCore"); + const firebasePackage = spmPackages.find( + (pkg) => pkg.name === "FirebaseCore", + ); assert.isDefined(firebasePackage, "Should include FirebaseCore package"); assert.equal( firebasePackage.version, @@ -58,9 +62,18 @@ describe("SPM Service - Package Override Logic", () => { "Should use app's FirebaseCore version (10.0.0), not plugin's (9.0.0)", ); - const alamofirePackage = spmPackages.find((pkg) => pkg.name === "Alamofire"); - assert.isDefined(alamofirePackage, "Should include Alamofire package from plugin"); - assert.equal(alamofirePackage.version, "5.0.0", "Should use plugin's Alamofire version"); + const alamofirePackage = spmPackages.find( + (pkg) => pkg.name === "Alamofire", + ); + assert.isDefined( + alamofirePackage, + "Should include Alamofire package from plugin", + ); + assert.equal( + alamofirePackage.version, + "5.0.0", + "Should use plugin's Alamofire version", + ); }); it("should include all plugin packages when no app packages exist", () => { @@ -84,10 +97,18 @@ describe("SPM Service - Package Override Logic", () => { const spmPackages = mergeSPMPackages(appPackages, pluginPackages); // Verify the result - assert.equal(spmPackages.length, 2, "Should include both plugin packages"); + assert.equal( + spmPackages.length, + 2, + "Should include both plugin packages", + ); const packageNames = spmPackages.map((pkg) => pkg.name); - assert.include(packageNames, "FirebaseCore", "Should include FirebaseCore"); + assert.include( + packageNames, + "FirebaseCore", + "Should include FirebaseCore", + ); assert.include(packageNames, "Alamofire", "Should include Alamofire"); }); @@ -159,3 +180,152 @@ describe("SPM Service - Package Override Logic", () => { }); }); }); + +describe("SPM Service - resolution log parsing", () => { + // describeSPMActivity / shortenPackageRef are pure helpers with no runtime + // dependencies, so we exercise the real implementation directly (the + // constructor only stashes injected services it never touches here). + const service: any = new (SPMService as any)(); + + describe("describeSPMActivity", () => { + it("flags the NativeScript runtime binary download specifically", () => { + assert.equal( + service.describeSPMActivity( + "Downloading binary artifact https://github.com/NativeScript/ios-spm/releases/download/9.0.3/NativeScript.xcframework.zip", + ), + "Downloading the NativeScript runtime (first build only)", + ); + }); + + it("flags other binary artifact downloads generically", () => { + assert.equal( + service.describeSPMActivity( + "Downloading binary artifact https://example.com/SomeSDK.xcframework.zip", + ), + "Downloading Swift Package binaries (first build only)", + ); + }); + + it("summarizes fetching with the package name in parentheses", () => { + assert.equal( + service.describeSPMActivity( + "Fetching from https://github.com/NativeScript/ios-spm.git", + ), + "Fetching Swift Packages (ios-spm)", + ); + }); + + it("summarizes cloning with the package name in parentheses", () => { + assert.equal( + service.describeSPMActivity( + "Cloning https://github.com/Alamofire/Alamofire.git", + ), + "Cloning Swift Packages (Alamofire)", + ); + }); + + it("omits the parenthesized name when the line has no URL", () => { + assert.equal( + service.describeSPMActivity("Fetching cached package"), + "Fetching Swift Packages", + ); + }); + + it("recognizes version computation", () => { + assert.equal( + service.describeSPMActivity( + "Computing version for https://github.com/NativeScript/ios-spm.git", + ), + "Computing package versions", + ); + }); + + it("recognizes the package graph resolution start", () => { + assert.equal( + service.describeSPMActivity("Resolve Package Graph"), + "Resolving Swift Package graph", + ); + }); + + it("recognizes the resolved/finalize step", () => { + assert.equal( + service.describeSPMActivity("Resolved source packages:"), + "Finalizing Swift Package dependencies", + ); + }); + + it("tolerates leading/trailing whitespace", () => { + assert.equal( + service.describeSPMActivity( + " Fetching https://github.com/NativeScript/ios-spm.git ", + ), + "Fetching Swift Packages (ios-spm)", + ); + }); + + it("returns null for blank lines", () => { + assert.isNull(service.describeSPMActivity("")); + assert.isNull(service.describeSPMActivity(" ")); + }); + + it("returns null for unrelated build output", () => { + assert.isNull( + service.describeSPMActivity("CompileSwift normal arm64 Foo.swift"), + ); + }); + }); + + describe("shortenPackageRef", () => { + it("extracts the repo name and strips the .git suffix", () => { + assert.equal( + service.shortenPackageRef( + "Fetching from https://github.com/NativeScript/ios-spm.git", + ), + "ios-spm", + ); + }); + + it("handles URLs without a .git suffix", () => { + assert.equal( + service.shortenPackageRef( + "Cloning https://github.com/Alamofire/Alamofire", + ), + "Alamofire", + ); + }); + + it("returns null when there is no URL", () => { + assert.isNull(service.shortenPackageRef("Fetching cached package")); + }); + }); + + describe("formatElapsed", () => { + it("always renders minutes and seconds", () => { + assert.equal(service.formatElapsed(0), "0m 0s"); + assert.equal(service.formatElapsed(42), "0m 42s"); + assert.equal(service.formatElapsed(60), "1m 0s"); + assert.equal(service.formatElapsed(315), "5m 15s"); + assert.equal(service.formatElapsed(3725), "62m 5s"); + }); + }); + + describe("formatPackageListing", () => { + it("lists one package per line using the platform EOL", () => { + assert.equal( + service.formatPackageListing([ + { + name: "FontManager", + version: "1.0.12", + repositoryURL: "https://github.com/NativeScript/font-manager.git", + }, + { name: "CanvasNative", path: "node_modules/canvas/ios" }, + ]), + "Swift Packages:" + + EOL + + " FontManager (1.0.12 · https://github.com/NativeScript/font-manager.git)" + + EOL + + " CanvasNative (local: node_modules/canvas/ios)", + ); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index 29545654d8..7be77bc26a 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -386,9 +386,7 @@ export class ErrorsStub implements IErrors { ): void {} } -export class PackageInstallationManagerStub - implements IPackageInstallationManager -{ +export class PackageInstallationManagerStub implements IPackageInstallationManager { clearInspectorCache(): void { return undefined; } @@ -733,11 +731,13 @@ export class ProjectDataStub implements IProjectData { public getAppDirectoryRelativePath(): string { return "app"; } + + public getBuildRelativeDirectoryPath(): string { + return constants.PLATFORMS_DIR_NAME; + } } -export class AndroidPluginBuildServiceStub - implements IAndroidPluginBuildService -{ +export class AndroidPluginBuildServiceStub implements IAndroidPluginBuildService { buildAar(options: IPluginBuildOptions): Promise { return Promise.resolve(true); } @@ -943,6 +943,20 @@ export class ProjectDataServiceStub implements IProjectDataService { return projectData; } + getProjectDataFromContent( + packageJsonContent: string, + projectDir?: string, + ): IProjectData { + const projectData = new ProjectDataStub(); + projectData.initializeProjectDataFromContent(); + + return projectData; + } + + getNsConfigDefaultContent(data?: Object): string { + return JSON.stringify({ ...data }); + } + async getAssetsStructure(opts: IProjectDir): Promise { return null; } @@ -1002,8 +1016,8 @@ export class ProjectTemplatesService implements IProjectTemplatesService { } export class HooksServiceStub implements IHooksService { - async executeBeforeHooks(commandName: string): Promise { - return Promise.resolve(); + async executeBeforeHooks(commandName: string): Promise { + return Promise.resolve([]); } async executeAfterHooks(commandName: string): Promise { @@ -1313,9 +1327,7 @@ export class CommandsService implements ICommandsService { } } -export class AndroidResourcesMigrationServiceStub - implements IAndroidResourcesMigrationService -{ +export class AndroidResourcesMigrationServiceStub implements IAndroidResourcesMigrationService { canMigrate(platformString: string): boolean { return true; } @@ -1329,9 +1341,7 @@ export class AndroidResourcesMigrationServiceStub } } -export class AndroidBundleValidatorHelper - implements IAndroidBundleValidatorHelper -{ +export class AndroidBundleValidatorHelper implements IAndroidBundleValidatorHelper { validateDeviceApiLevel(device: Mobile.IDevice, buildData: IBuildData): void { return; } diff --git a/test/sys-info.ts b/test/sys-info.ts index 0433b4df04..f2f9d31ef1 100644 --- a/test/sys-info.ts +++ b/test/sys-info.ts @@ -12,6 +12,7 @@ import { ISysInfo, IFileSystem, } from "../lib/common/declarations"; +import { SystemWarningsSeverity } from "../lib/definitions/system-warnings"; const verifyNodeVersion = require("../lib/common/verify-node-version"); describe("sysInfo", () => { @@ -49,8 +50,8 @@ describe("sysInfo", () => { ? { message: opts.nodeJsWarning, severity: SystemWarningsSeverity.medium, - } - : null + } + : null, ); const testInjector = createTestInjector(); @@ -111,7 +112,7 @@ describe("sysInfo", () => { describe("getMacOSWarningMessage", () => { const getMacOSWarning = async ( - macOSDeprecatedVersion?: string + macOSDeprecatedVersion?: string, ): Promise => { sandbox.stub(verifyNodeVersion, "getNodeWarning").returns(null); diff --git a/test/tools/config-manipulation/config-transformer.ts b/test/tools/config-manipulation/config-transformer.ts index 3ecc6bd5d0..e2e2463839 100644 --- a/test/tools/config-manipulation/config-transformer.ts +++ b/test/tools/config-manipulation/config-transformer.ts @@ -59,4 +59,148 @@ export default { spmPackages, ); }); + + const tsConfig = `export default { + id: 'org.nativescript.myapp', + appPath: 'src', + version: 3, +} as any;`; + + const roundTrip = (content: string, path: string, value: any) => + new ConfigTransformer( + new ConfigTransformer(content).setValue(path, value), + ).getValue(path); + + it("reads and updates string literals", () => { + assert.strictEqual( + new ConfigTransformer(tsConfig).getValue("id"), + "org.nativescript.myapp", + ); + assert.strictEqual(roundTrip(tsConfig, "appPath", "app"), "app"); + }); + + it("reads and updates numeric literals", () => { + assert.strictEqual(new ConfigTransformer(tsConfig).getValue("version"), 3); + assert.strictEqual(roundTrip(tsConfig, "version", 4), 4); + }); + + it("replaces the initializer when the new value changes type", () => { + assert.strictEqual(roundTrip(tsConfig, "version", "four"), "four"); + assert.strictEqual(roundTrip(tsConfig, "appPath", 7), 7); + }); + + it("reads and updates CommonJS configs", () => { + const content = `module.exports = { + id: 'org.nativescript.myapp', + appPath: 'src', +};`; + + assert.strictEqual( + new ConfigTransformer(content).getValue("id"), + "org.nativescript.myapp", + ); + assert.strictEqual(roundTrip(content, "appPath", "app"), "app"); + }); + + // the object may be wrapped in any combination of assertions and parentheses + const wrappers: [string, string][] = [ + ["no assertion", `{ id: 'org.nativescript.myapp' }`], + [ + "as NativeScriptConfig", + `{ id: 'org.nativescript.myapp' } as NativeScriptConfig`, + ], + ["as any", `{ id: 'org.nativescript.myapp' } as any`], + ["as const", `{ id: 'org.nativescript.myapp' } as const`], + [ + "satisfies", + `{ id: 'org.nativescript.myapp' } satisfies NativeScriptConfig`, + ], + ["angle-bracket assertion", `{ id: 'org.nativescript.myapp' }`], + ["parenthesized", `({ id: 'org.nativescript.myapp' })`], + [ + "nested parens and assertion", + `(({ id: 'org.nativescript.myapp' } as any))`, + ], + ]; + + for (const [label, expression] of wrappers) { + it(`reads and updates a default export wrapped in ${label}`, () => { + const content = `export default ${expression};`; + + assert.strictEqual( + new ConfigTransformer(content).getValue("id"), + "org.nativescript.myapp", + ); + assert.strictEqual(roundTrip(content, "id", "org.other"), "org.other"); + }); + } + + it("reads and updates a parenthesized CommonJS export", () => { + const content = `module.exports = ({ + id: 'org.nativescript.myapp', +});`; + + assert.strictEqual( + new ConfigTransformer(content).getValue("id"), + "org.nativescript.myapp", + ); + assert.strictEqual(roundTrip(content, "id", "org.other"), "org.other"); + }); + + it("creates intermediate objects for a new dot-notation path", () => { + assert.strictEqual( + roundTrip(tsConfig, "android.markingMode", "none"), + "none", + ); + }); + + it("adds keys that are absent from the config", () => { + assert.strictEqual( + roundTrip(tsConfig, "appResourcesPath", "App_Resources"), + "App_Resources", + ); + assert.deepStrictEqual( + roundTrip(tsConfig, "ios", { discardUncaughtJsExceptions: true }), + { + discardUncaughtJsExceptions: true, + }, + ); + }); + + it("resolves a value declared as a separate variable", () => { + const content = `const appId = 'org.nativescript.myapp'; + +export default { + id: appId, +} as any;`; + + assert.strictEqual( + new ConfigTransformer(content).getValue("id"), + "org.nativescript.myapp", + ); + // the assignment is indirect, so the update lands on the declaration + const updated = new ConfigTransformer(content).setValue("id", "org.other"); + assert.include(updated, "const appId = 'org.other'"); + assert.strictEqual( + new ConfigTransformer(updated).getValue("id"), + "org.other", + ); + }); + + it("returns undefined for a key that is not present", () => { + assert.isUndefined( + new ConfigTransformer(tsConfig).getValue("doesNotExist"), + ); + }); + + it("throws when the default export is not an object", () => { + assert.throws( + () => new ConfigTransformer(`export default 42;`).getValue("id"), + "default export must be an object!", + ); + assert.throws( + () => new ConfigTransformer(`module.exports = 42;`).getValue("id"), + "default export must be an object!", + ); + }); }); diff --git a/test/tools/plist-merge/plist-session.ts b/test/tools/plist-merge/plist-session.ts new file mode 100644 index 0000000000..cac3ee270a --- /dev/null +++ b/test/tools/plist-merge/plist-session.ts @@ -0,0 +1,159 @@ +import { assert } from "chai"; +import * as plist from "plist"; +import { + PlistSession, + Reporter, +} from "../../../lib/tools/plist-merge/plist-session"; + +const build = (patches: any[], reporter?: Reporter) => { + const session = new PlistSession(reporter); + + patches.forEach((patch, index) => + session.patch({ name: `patch-${index}`, read: () => plist.build(patch) }), + ); + + return session.build(); +}; + +const merge = (patches: any[], reporter?: Reporter): any => + plist.parse(build(patches, reporter)); + +describe("PlistSession", () => { + it("reports no patches until one is scheduled", () => { + const session = new PlistSession(); + assert.isFalse(session.hasPatches); + + session.patch({ name: "p", read: () => plist.build({ A: "1" }) }); + assert.isTrue(session.hasPatches); + }); + + it("builds a plist from a single patch", () => { + assert.deepStrictEqual(merge([{ CFBundleName: "app" }]), { + CFBundleName: "app", + }); + }); + + it("lets a later patch overwrite a scalar", () => { + assert.deepStrictEqual(merge([{ A: "1", B: "keep" }, { A: "2" }]), { + A: "2", + B: "keep", + }); + }); + + it("merges nested objects rather than replacing them", () => { + assert.deepStrictEqual( + merge([{ N: { x: "1", y: "2" } }, { N: { y: "9", z: "3" } }]), + { N: { x: "1", y: "9", z: "3" } }, + ); + }); + + it("replaces plain arrays instead of concatenating them", () => { + // lodash would merge these element-wise, which is not what a plist patch means + assert.deepStrictEqual(merge([{ Arr: ["a", "b", "c"] }, { Arr: ["z"] }]), { + Arr: ["z"], + }); + }); + + describe("CFBundleURLTypes", () => { + it("folds schemes into an entry that declares the same role", () => { + const result = merge([ + { + CFBundleURLTypes: [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["a"] }, + ], + }, + { + CFBundleURLTypes: [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["b"] }, + ], + }, + ]); + + assert.deepStrictEqual(result.CFBundleURLTypes, [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["a", "b"] }, + ]); + }); + + it("appends an entry declaring a different role", () => { + const result = merge([ + { + CFBundleURLTypes: [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["a"] }, + ], + }, + { + CFBundleURLTypes: [ + { CFBundleTypeRole: "Viewer", CFBundleURLSchemes: ["b"] }, + ], + }, + ]); + + assert.deepStrictEqual(result.CFBundleURLTypes, [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["a"] }, + { CFBundleTypeRole: "Viewer", CFBundleURLSchemes: ["b"] }, + ]); + }); + + it("accumulates schemes across three patches", () => { + const patchFor = (scheme: string) => ({ + CFBundleURLTypes: [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: [scheme] }, + ], + }); + + const result = merge([patchFor("a"), patchFor("b"), patchFor("c")]); + + assert.deepStrictEqual(result.CFBundleURLTypes, [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["a", "b", "c"] }, + ]); + }); + + it("warns when an entry omits the role it would be matched on", () => { + const warnings: string[] = []; + const result = merge( + [ + { CFBundleURLTypes: [{ CFBundleURLSchemes: ["a"] }] }, + { + CFBundleURLTypes: [ + { CFBundleTypeRole: "Editor", CFBundleURLSchemes: ["b"] }, + ], + }, + ], + { warn: (msg: string) => warnings.push(msg) }, + ); + + assert.lengthOf(warnings, 1); + assert.include(warnings[0], "CFBundleTypeRole is required"); + // the roles do not match, so the patch is appended rather than folded in + assert.lengthOf(result.CFBundleURLTypes, 2); + }); + }); + + describe("LSApplicationQueriesSchemes", () => { + it("unions schemes and drops duplicates", () => { + const result = merge([ + { LSApplicationQueriesSchemes: ["a", "b"] }, + { LSApplicationQueriesSchemes: ["b", "c"] }, + ]); + + assert.deepStrictEqual(result.LSApplicationQueriesSchemes, [ + "a", + "b", + "c", + ]); + }); + }); + + it("reports progress through the reporter", () => { + const messages: string[] = []; + build([{ A: "1" }], { log: (msg: string) => messages.push(msg) }); + + assert.include(messages, "Start"); + assert.include(messages, "Complete"); + assert.include(messages, "Patch 'patch-0'"); + }); + + it("works without a reporter", () => { + assert.deepStrictEqual(merge([{ A: "1" }]), { A: "1" }); + }); +}); diff --git a/test/type-fixtures/define-command-types.ts b/test/type-fixtures/define-command-types.ts new file mode 100644 index 0000000000..e8d1f80d49 --- /dev/null +++ b/test/type-fixtures/define-command-types.ts @@ -0,0 +1,84 @@ +/** + * Type-level assertions for the defineCommand schema, compiled by + * test/define-command.ts through this directory's tsconfig. It is kept out of + * the repo's own build because that build runs without strictNullChecks, which + * erases the `| undefined` these assertions exist to pin — and because the + * @ts-expect-error directives below only hold under strict mode. + */ + +import { + arrayOption, + booleanOption, + defineCommand, + numberOption, + stringOption, +} from "../../lib/common/define-command"; + +type IsExact = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false; + +const expectExactType = (): void => undefined; + +// A declared option is `T` only when the schema supplies a default; without +// one the flag may simply be absent from the command line. +defineCommand({ + name: "typefixture|values", + options: { + verbose: booleanOption(), + release: booleanOption({ default: false }), + output: stringOption({ alias: "o" }), + target: stringOption({ default: "dist" }), + retries: numberOption(), + attempts: numberOption({ default: 3 }), + files: arrayOption(), + tags: arrayOption({ default: [] }), + }, + run(ctx) { + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType>(); + + // @ts-expect-error - the schema types ctx.options and nothing else + ctx.options.undeclared; + }, +}); + +defineCommand({ + name: "typefixture|no-options", + run(ctx) { + expectExactType>(); + // `never` is what lets fail() end a branch without a return. + expectExactType, never>>(); + + // @ts-expect-error - nothing is declared, so any access is a typo + ctx.options.anything; + }, +}); + +defineCommand({ + name: "typefixture|refine", + options: { force: booleanOption({ default: false }) }, + canExecute(ctx) { + expectExactType>(); + return ctx.args.length === 1; + }, + run: () => undefined, +}); + +// @ts-expect-error - `run` is the required handler field +defineCommand({ name: "typefixture|no-run" }); + +defineCommand({ + name: "typefixture|bad-arguments", + // @ts-expect-error - `arguments` is a closed set + arguments: "one", + run: () => undefined, +}); diff --git a/test/type-fixtures/tsconfig.json b/test/type-fixtures/tsconfig.json new file mode 100644 index 0000000000..254bd447c0 --- /dev/null +++ b/test/type-fixtures/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": false, + "lib": ["ESNext"], + "types": [] + }, + "files": ["define-command-types.ts"] +} diff --git a/test/vitest-globals.d.ts b/test/vitest-globals.d.ts new file mode 100644 index 0000000000..9896c472fb --- /dev/null +++ b/test/vitest-globals.d.ts @@ -0,0 +1 @@ +/// diff --git a/test/xcconfig-service.ts b/test/xcconfig-service.ts index a43291f73d..20d9c20ce0 100644 --- a/test/xcconfig-service.ts +++ b/test/xcconfig-service.ts @@ -5,6 +5,7 @@ import * as yok from "../lib/common/yok"; import { IXcconfigService } from "../lib/declarations"; import { IInjector } from "../lib/common/definitions/yok"; import { IReadFileOptions } from "../lib/common/declarations"; +import { LoggerStub } from "./stubs"; // start tracking temporary folders/files @@ -18,6 +19,7 @@ describe("XCConfig Service Tests", () => { }); testInjector.register("childProcess", {}); testInjector.register("xcprojService", {}); + testInjector.register("logger", LoggerStub); testInjector.register("xcconfigService", XcconfigService); diff --git a/tsconfig.json b/tsconfig.json index 62f5889ae1..45bf605cf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,15 +2,25 @@ "compilerOptions": { "target": "ES2018", "module": "commonjs", + // rootDir is explicit so output keeps the lib/ and test/ layout; without + // it tsc infers the common source root and flattens everything + "rootDir": ".", + "outDir": "dist", "sourceMap": true, "declaration": false, "removeComments": false, "noImplicitAny": true, "experimentalDecorators": true, + "isolatedModules": true, "skipLibCheck": true, "alwaysStrict": true, "noUnusedLocals": true, - "lib": ["ESNext"] + "lib": ["ESNext"], + "strict": false }, - "include": ["lib/", "test/"] + // test/type-fixtures has its own strict tsconfig and is compiled by the test + // that asserts on it; building it here would emit a testless file into dist + // and drop the strictness those assertions depend on + "include": ["lib/", "test/"], + "exclude": ["test/type-fixtures/"] } diff --git a/tsconfig.release.json b/tsconfig.release.json new file mode 100644 index 0000000000..5d7991c1e0 --- /dev/null +++ b/tsconfig.release.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "removeComments": true, + "declaration": true + }, + "include": ["lib/"], + "exclude": ["lib/common/test"] +} diff --git a/vendor/aab-tool/LICENSE b/vendor/aab-tool/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/vendor/aab-tool/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/aab-tool/README.txt b/vendor/aab-tool/README.txt deleted file mode 100644 index c57c515602..0000000000 --- a/vendor/aab-tool/README.txt +++ /dev/null @@ -1 +0,0 @@ -Downloaded from https://github.com/google/bundletool/releases/tag/1.18.2 \ No newline at end of file diff --git a/vendor/aab-tool/bundletool.jar b/vendor/aab-tool/bundletool.jar deleted file mode 100644 index b95c3beb9b..0000000000 Binary files a/vendor/aab-tool/bundletool.jar and /dev/null differ diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..f1ff097b39 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "vitest/config"; + +// Tests run against tsc's output in dist/ rather than the TypeScript sources: +// the injector discovers dependencies by regex-parsing constructor source text +// (see annotate() in lib/common/helpers.ts), which only matches tsc's emit. +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["dist/test/**/*.js", "dist/lib/common/test/unit-tests/**/*.js"], + exclude: [ + "**/node_modules/**", + "dist/test/files/**", + "dist/test/stubs.js", + "dist/test/test-bootstrap.js", + "dist/test/base-service-test.js", + "dist/lib/common/test/unit-tests/stubs.js", + "dist/lib/common/test/unit-tests/mocks/**", + "dist/lib/common/test/with-done.js", + ], + setupFiles: ["./dist/test/test-bootstrap.js"], + testTimeout: 150000, + hookTimeout: 150000, + }, +});