diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 22ac9d552d2..b2e8c8a1064 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,6 +44,26 @@ updates: semver-patch-days: 7 semver-minor-days: 7 semver-major-days: 14 + # Major updates are held back for production dependencies, the same rule + # the bun workspace entry below applies across the workspace. + # @vendure-io/docs-provider is the only name this entry needs. + # @types/node, tsx and typescript are development dependencies and keep + # offering majors. + # + # This is an ignore rule where the bun entry uses an allow list, and the + # difference is deliberate. An allow list narrows security updates as + # well as version updates. npm is an ecosystem where Dependabot raises + # security updates, and the documented default covers every vulnerable + # dependency in the lock file, transitive ones included. No + # `dependency-type` names that set for npm — `all` stops at explicitly + # declared dependencies — so an allow list here would have dropped + # advisories for anything transitive under @vendure-io/docs-provider. + # `update-types` under ignore applies to version updates only, so this + # rule leaves security updates untouched. + ignore: + - dependency-name: '@vendure-io/docs-provider' + update-types: + - version-update:semver-major groups: docs: patterns: @@ -83,6 +103,162 @@ updates: # range, so a patch update always falls inside the existing range and no # strategy rewrites the manifest. versioning-strategy: increase-if-necessary + # Major updates are raised for development tooling only. A major version + # of a package that ships to users is a migration for every Vendure + # project downstream, so it belongs in a planned release. + # + # The rule is written as a list of dependency names rather than + # `dependency-type: development`, for two independent reasons. + # + # First, GitHub documents the `dependency-type` values `production` and + # `development` for bundler, composer, mix, maven, npm, pip and uv. bun + # is absent. That list differs from the one above for + # versioning-strategy, because Dependabot documents support per option + # rather than per ecosystem. + # + # Second, the workspace manifests do not split along that line, in either + # direction. eslint, eslint-config-prettier and the @typescript-eslint + # packages are production dependencies of the root manifest, typescript + # and ts-node of packages/cli, @types/express of core and + # asset-server-plugin, so a rule scoped to development dependencies + # would block tooling majors. dependabot_auto_merge.yml rejected + # dependency-type as an auto-merge gate for the same reason. + # + # The reverse holds too, which is why this list names tooling rather + # than every development dependency. bullmq, ioredis, pg, mysql2 and + # better-sqlite3 are declared as development dependencies here, but + # plugins peer-depend on them, so a major is a downstream migration. + # + # An allow list replaces the default set rather than narrowing it, so the + # first entry has to restate the default: every dependency gets minor and + # patch updates. Each entry after it adds majors back for one tooling + # family. The runtime families — nestjs, graphql, typeorm, opentelemetry + # and the dashboard packages — are absent on purpose. + # + # A tooling package with no entry here stops offering majors, and the + # symptom is a pull request that never arrives, which nobody notices. + # When adding a development tool to any manifest, add it here too. + # + # ignore beats allow: a dependency matched by both is ignored. The + # Angular families in the ignore block below therefore stay ignored at + # every level, and the '*' entry here does not re-enable them. + # + # Narrowing the set costs nothing here, because Dependabot raises no + # security updates for bun at all, as noted above. That is not true of + # npm, which is why the /docs entry states its rule as an ignore. + allow: + - dependency-name: '*' + update-types: + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: 'typescript' + update-types: + - version-update:semver-major + - dependency-name: '@types/*' + update-types: + - version-update:semver-major + - dependency-name: 'eslint' + update-types: + - version-update:semver-major + - dependency-name: 'eslint-*' + update-types: + - version-update:semver-major + - dependency-name: '@typescript-eslint/*' + update-types: + - version-update:semver-major + - dependency-name: 'prettier' + update-types: + - version-update:semver-major + - dependency-name: 'prettier-*' + update-types: + - version-update:semver-major + - dependency-name: 'vitest' + update-types: + - version-update:semver-major + - dependency-name: '@vitest/*' + update-types: + - version-update:semver-major + - dependency-name: 'rimraf' + update-types: + - version-update:semver-major + - dependency-name: 'concurrently' + update-types: + - version-update:semver-major + - dependency-name: 'cross-env' + update-types: + - version-update:semver-major + - dependency-name: 'ts-node' + update-types: + - version-update:semver-major + # Tooling that belongs to no group above, so a major arrives on its + # own. @eslint/js and globals are listed because the flat config + # requires them to move with an eslint major. + - dependency-name: '@eslint/js' + update-types: + - version-update:semver-major + - dependency-name: 'globals' + update-types: + - version-update:semver-major + - dependency-name: '@commitlint/*' + update-types: + - version-update:semver-major + - dependency-name: 'husky' + update-types: + - version-update:semver-major + - dependency-name: 'lint-staged' + update-types: + - version-update:semver-major + - dependency-name: 'lerna' + update-types: + - version-update:semver-major + - dependency-name: 'conventional-changelog-core' + update-types: + - version-update:semver-major + - dependency-name: '@playwright/test' + update-types: + - version-update:semver-major + - dependency-name: 'playwright' + update-types: + - version-update:semver-major + - dependency-name: 'jsdom' + update-types: + - version-update:semver-major + - dependency-name: 'tinybench' + update-types: + - version-update:semver-major + - dependency-name: 'storybook' + update-types: + - version-update:semver-major + - dependency-name: '@storybook/*' + update-types: + - version-update:semver-major + - dependency-name: '@graphql-codegen/*' + update-types: + - version-update:semver-major + - dependency-name: '@gql.tada/cli-utils' + update-types: + - version-update:semver-major + - dependency-name: '@swc/core' + update-types: + - version-update:semver-major + - dependency-name: 'unplugin-swc' + update-types: + - version-update:semver-major + - dependency-name: 'rollup' + update-types: + - version-update:semver-major + - dependency-name: '@rollup/*' + update-types: + - version-update:semver-major + - dependency-name: 'rollup-plugin-typescript2' + update-types: + - version-update:semver-major + - dependency-name: 'vite-plugin-dts' + update-types: + - version-update:semver-major + - dependency-name: '@typescript/native-preview' + update-types: + - version-update:semver-major ignore: # Workspace-internal packages are versioned by the release process. - dependency-name: '@vendure/*' diff --git a/.github/workflows/back_merge.yml b/.github/workflows/back_merge.yml index dd6d8e9959d..d0bd02402a2 100644 --- a/.github/workflows/back_merge.yml +++ b/.github/workflows/back_merge.yml @@ -57,7 +57,7 @@ jobs: steps: - name: Generate CI Bot Token id: app-token - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.CI_BOT_APP_ID }} private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} @@ -66,7 +66,7 @@ jobs: permission-contents: write - name: Checkout target branch - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.app-token.outputs.token }} ref: ${{ needs.resolve-branches.outputs.target }} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index a1ef528bc05..0169e1fe5f0 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -38,7 +38,7 @@ jobs: packages: ${{ steps.check.outputs.packages }} dashboard: ${{ steps.check.outputs.dashboard }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Detect changed paths @@ -82,7 +82,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - name: Build run: bun run build @@ -98,7 +98,7 @@ jobs: matrix: node: [20.x, 22.x, 24.x] steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node }} @@ -114,7 +114,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - name: Check i18n catalogs are in sync working-directory: packages/dashboard @@ -131,13 +131,13 @@ jobs: matrix: shard: [1, 2, 3, 4] steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - name: Build run: bunx lerna run ci --scope @vendure/testing --include-dependencies - name: Cache Playwright browsers id: playwright-cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/ms-playwright # The key version prefix (v2) lets us evict caches poisoned by an @@ -182,11 +182,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6 + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} e2e-sqljs: @@ -206,7 +206,7 @@ jobs: matrix: node: ${{ github.event_name == 'pull_request' && fromJSON('["22.x"]') || fromJSON('["20.x", "22.x", "24.x"]') }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node }} @@ -243,7 +243,7 @@ jobs: matrix: node: ${{ github.event_name == 'pull_request' && fromJSON('["22.x"]') || fromJSON('["20.x", "22.x", "24.x"]') }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node }} @@ -291,7 +291,7 @@ jobs: matrix: node: ${{ github.event_name == 'pull_request' && fromJSON('["22.x"]') || fromJSON('["20.x", "22.x", "24.x"]') }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node }} @@ -328,7 +328,7 @@ jobs: matrix: node: ${{ github.event_name == 'pull_request' && fromJSON('["22.x"]') || fromJSON('["20.x", "22.x", "24.x"]') }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 08614ab34ae..843e40de0b2 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Generate CI Bot Token id: app-token - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.CI_BOT_APP_ID }} private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} @@ -36,7 +36,7 @@ jobs: # via mutable version tags. See https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@9340315624c6e16cef1f2c63bdeb0f0c49c6f474 # v2.4.0 + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} with: diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index d5b04a879b1..4e141ff7010 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -12,9 +12,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js 22 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/deploy_dashboard.yml b/.github/workflows/deploy_dashboard.yml index e41d4f63e25..ad8f38c1e3c 100644 --- a/.github/workflows/deploy_dashboard.yml +++ b/.github/workflows/deploy_dashboard.yml @@ -36,7 +36,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup environment uses: ./.github/actions/setup diff --git a/.github/workflows/docs_ci.yml b/.github/workflows/docs_ci.yml index 153cbfdc287..dc4039dbde2 100644 --- a/.github/workflows/docs_ci.yml +++ b/.github/workflows/docs_ci.yml @@ -49,7 +49,7 @@ jobs: working-directory: docs steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -78,7 +78,7 @@ jobs: steps: - name: Generate CI Bot Token id: app-token - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.CI_BOT_APP_ID }} private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} @@ -86,7 +86,7 @@ jobs: permission-contents: write - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.app-token.outputs.token }} # Check out the branch by name rather than the pushed SHA so the diff --git a/.github/workflows/floating_deps_check.yml b/.github/workflows/floating_deps_check.yml new file mode 100644 index 00000000000..29527b9eebe --- /dev/null +++ b/.github/workflows/floating_deps_check.yml @@ -0,0 +1,114 @@ +name: Floating dependency check + +# Builds the published packages against a fresh resolve of their declared dependency ranges, +# with no lockfile. +# +# build_and_test.yml installs with --frozen-lockfile, so it verifies the one version combination +# recorded in bun.lock. No consumer installs that combination. packages/dashboard, for one, sets +# "main" and "module" to ./src/lib/index.ts, so consumers compile its TypeScript source against +# their own resolution of the ranges it declares. This job checks the thing consumers actually do. +# +# It is deliberately not a required check, runs on a schedule rather than on pull requests, and +# gates nothing. It fails whenever a third party ships a release inside a declared range that does +# not compile, which is information rather than a reason to block other people's work. +on: + schedule: + # Wednesday 04:00 UTC. Offset from the Monday Dependabot run so a failure here is not read + # as being caused by that morning's pull requests, and away from the 02:00 jobs. + - cron: '0 4 * * 3' + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + resolve-and-build: + name: resolve declared ranges and build + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # No push credentials are left on disk. This job must never write to the + # repository, and the throwaway resolve must never be committed. + persist-credentials: false + + # .github/actions/setup is not used here because it installs with --frozen-lockfile, + # which is the thing this job exists to avoid. The Node and Bun pins below are kept in + # step with that action. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22.x' + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.3.10' + + - name: Resolve declared ranges with no lockfile + shell: bash + run: | + # Deleting the lockfile is what forces a fresh resolve. Without this, bun installs + # the pinned versions and the job verifies the same thing build_and_test already + # does. + rm -f bun.lock + + # bunfig.toml sets install.minimumReleaseAge to seven days, so a resolve in this + # repository skips anything published in the last week. A consumer installing + # with npm, pnpm or yarn has no such gate and gets the newest matching version + # straight away. Passing --minimum-release-age=0 on the command line overrides + # the bunfig value for this run only, without editing the committed file. The + # committed value stays at seven days for everyone else. + # + # --no-save stops bun writing a replacement bun.lock. A later step asserts that. + bun install --minimum-release-age=0 --no-save 2>&1 | tee /tmp/floating-deps.log + + - name: Record resolved versions + if: always() + shell: bash + run: node .github/workflows/scripts/floating-deps-report.js resolved | tee /tmp/resolved.md + + - name: Build and type check + shell: bash + run: bun run build 2>&1 | tee -a /tmp/floating-deps.log + + - name: Confirm no lockfile was written + if: always() + shell: bash + run: | + if [ -f bun.lock ]; then + echo "bun.lock was regenerated, so --no-save did not take effect." + echo "The file is not committed from here, but the flag is meant to prevent it existing at all." + exit 1 + fi + echo "No lockfile was written." + + # The issue body truncates both files to fit GitHub's 65536 character limit. Keeping + # them whole here means a truncated report still has somewhere to point. + - name: Upload the resolved versions and the build output + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: floating-deps-${{ github.run_id }} + path: | + /tmp/resolved.md + /tmp/floating-deps.log + if-no-files-found: warn + retention-days: 30 + + - name: Open or update the failure issue + if: failure() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RESOLVED_FILE: /tmp/resolved.md + LOG_FILE: /tmp/floating-deps.log + run: node .github/workflows/scripts/floating-deps-report.js report + + - name: Close the failure issue + if: success() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/workflows/scripts/floating-deps-report.js resolve-issue diff --git a/.github/workflows/generate_docs.yml b/.github/workflows/generate_docs.yml index 7c3ab21dc00..1da861f6637 100644 --- a/.github/workflows/generate_docs.yml +++ b/.github/workflows/generate_docs.yml @@ -47,7 +47,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate and validate docs uses: ./.github/actions/generate-docs @@ -62,7 +62,7 @@ jobs: steps: - name: Generate CI Bot Token id: app-token - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.CI_BOT_APP_ID }} private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} @@ -70,7 +70,7 @@ jobs: permission-contents: write - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.app-token.outputs.token }} # Check out the branch by name rather than the pushed SHA so the @@ -103,7 +103,7 @@ jobs: if: steps.diff.outputs.changed == 'true' id: ai-message continue-on-error: true - uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8 + uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3 with: model: openai/gpt-4o-mini max-tokens: 100 diff --git a/.github/workflows/lint_pr_title.yml b/.github/workflows/lint_pr_title.yml index c5c18fdaac4..8e8cab56de8 100644 --- a/.github/workflows/lint_pr_title.yml +++ b/.github/workflows/lint_pr_title.yml @@ -12,7 +12,7 @@ jobs: name: conventional commit format runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/setup - name: Lint pull request title env: diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 90d26c4dfca..69c37218bab 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -38,9 +38,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js 22.x - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.x - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -74,7 +74,7 @@ jobs: cd $HOME/.config/verdaccio tar -czf verdaccio-storage.tar.gz storage htpasswd - name: Upload Verdaccio storage - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: verdaccio-storage path: ~/.config/verdaccio/verdaccio-storage.tar.gz @@ -97,13 +97,13 @@ jobs: node-version: ${{ github.event_name == 'pull_request' && fromJSON('["22.x"]') || fromJSON('["22.x", "24.x", "26.x"]') }} fail-fast: false steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} - name: Download Verdaccio storage - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: verdaccio-storage path: ~/verdaccio-download @@ -179,7 +179,7 @@ jobs: cd ~/install/test-app node setup-test-plugin.js - name: Cache Playwright browsers - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} @@ -209,7 +209,7 @@ jobs: kill $DEV_PID 2>/dev/null || true - name: Upload dashboard test screenshots if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dashboard-test-screenshots-${{ matrix.os }}-${{ matrix.node-version }} path: /tmp/dashboard-test-*.png @@ -242,9 +242,9 @@ jobs: scaffold: yarn dlx @vendure/create@$CREATE_VERSION test-app --ci --log-level info fail-fast: false steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js 22.x - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.x - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -261,7 +261,7 @@ jobs: pnpm --version yarn --version - name: Download Verdaccio storage - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: verdaccio-storage path: ~/verdaccio-download @@ -339,13 +339,13 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js 22.x - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.x - name: Download Verdaccio storage - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: verdaccio-storage path: ~/verdaccio-download diff --git a/.github/workflows/publish_to_npm.yml b/.github/workflows/publish_to_npm.yml index 29f9a3a70b6..c6bf9b2f482 100644 --- a/.github/workflows/publish_to_npm.yml +++ b/.github/workflows/publish_to_npm.yml @@ -77,7 +77,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ matrix.type == 'minor-nightly' && 'minor' || matrix.type == 'master-nightly' && 'master' || '' }} fetch-depth: ${{ matrix.type == 'release' && 1 || 0 }} @@ -97,7 +97,7 @@ jobs: - name: Setup Node.js if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/scripts/floating-deps-report.js b/.github/workflows/scripts/floating-deps-report.js new file mode 100644 index 00000000000..e654ba74dff --- /dev/null +++ b/.github/workflows/scripts/floating-deps-report.js @@ -0,0 +1,246 @@ +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * Support script for floating_deps_check.yml. + * + * `node floating-deps-report.js resolved` prints a table of every dependency range the published + * packages declare, next to the version a fresh resolve picked for it. + * + * `node floating-deps-report.js report` opens an issue describing a failed run, or edits the + * issue an earlier failed run already opened. `node floating-deps-report.js resolve-issue` closes + * that issue once a run passes again. + * + * A package counts as published when its path is packages//package.json and its manifest + * does not set `"private": true`, which is the same test dependency-impact.js uses. + */ + +const MARKER = ''; +const LABEL = 'ci: floating deps'; +const TITLE = 'Scheduled floating dependency check is failing'; +const CONTRACT_SECTIONS = ['dependencies', 'peerDependencies', 'optionalDependencies']; +// GitHub rejects an issue body over 65536 characters, and the body carries both the resolved +// versions and the build output. These two caps leave headroom for the surrounding text. The +// workflow also uploads both files whole as a run artifact, so truncation here loses nothing. +const MAX_RESOLVED_CHARS = 20000; +const MAX_LOG_CHARS = 20000; + +const repo = process.env.GITHUB_REPOSITORY; +const mode = process.argv[2]; + +if (mode === 'resolved') { + process.stdout.write(resolvedTable()); +} else if (mode === 'report') { + report(); +} else if (mode === 'resolve-issue') { + resolveIssue(); +} else { + console.error(`unknown mode "${mode}", expected resolved, report or resolve-issue`); + process.exit(1); +} + +function publishedPackages() { + return fs + .readdirSync('packages', { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => path.join('packages', entry.name, 'package.json')) + .filter(manifestPath => fs.existsSync(manifestPath)) + .map(manifestPath => ({ manifestPath, manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')) })) + .filter(({ manifest }) => manifest.private !== true); +} + +/** + * Finds the version actually installed for a dependency of a given package. bun hoists most + * packages to the root node_modules but can nest one under the package that needs it, so check + * the nested location first. + */ +function installedVersion(packageDir, name) { + const candidates = [ + path.join(packageDir, 'node_modules', name, 'package.json'), + path.join('node_modules', name, 'package.json'), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + try { + return JSON.parse(fs.readFileSync(candidate, 'utf8')).version; + } catch (e) { + return `unreadable (${e.message})`; + } + } + } + return null; +} + +function resolvedTable() { + const rows = []; + for (const { manifestPath, manifest } of publishedPackages()) { + const packageDir = path.dirname(manifestPath); + for (const section of CONTRACT_SECTIONS) { + for (const [name, range] of Object.entries(manifest[section] || {})) { + // Workspace packages resolve to the checkout, so their version says nothing about + // what a consumer would get. + if (typeof range === 'string' && range.startsWith('workspace:')) { + continue; + } + rows.push({ + pkg: manifest.name, + section, + name, + range, + resolved: installedVersion(packageDir, name), + }); + } + } + } + rows.sort((a, b) => a.pkg.localeCompare(b.pkg) || a.name.localeCompare(b.name)); + + const lines = ['| package | dependency | declared range | resolved |', '| --- | --- | --- | --- |']; + for (const row of rows) { + const section = row.section === 'dependencies' ? '' : ` _(${row.section})_`; + lines.push( + `| \`${row.pkg}\` | \`${row.name}\`${section} | \`${row.range}\` | ${ + row.resolved ? `\`${row.resolved}\`` : 'not installed' + } |`, + ); + } + return lines.join('\n') + '\n'; +} + +/** + * Reads a file, keeping the end when it is too long. The end of a build log holds the error that + * stopped it, which is the part worth reading. + */ +function readTail(file, limit) { + if (!file || !fs.existsSync(file)) { + return '(no output captured)'; + } + const text = fs.readFileSync(file, 'utf8'); + if (text.length <= limit) { + return text || '(empty)'; + } + return `... truncated to the last ${limit} characters, see the run artifact for the whole file ...\n${text.slice( + -limit, + )}`; +} + +function buildBody() { + const runUrl = `${process.env.GITHUB_SERVER_URL}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + return [ + MARKER, + '', + 'The scheduled floating dependency check failed. This job resolves the published packages', + 'declared dependency ranges with no lockfile and builds the result, so it reports what a', + 'consumer installing these packages today would get. It is not a required check and blocks', + 'nothing.', + '', + 'A failure here usually means a third party shipped a release that is inside a declared', + 'range and does not compile against this source. The fix is either to correct the range or', + 'to update the code that uses it. Failing on its own is not a reason to revert anything.', + '', + `Run: ${runUrl}`, + `Commit: \`${process.env.GITHUB_SHA}\``, + '', + '## Resolved versions', + '', + '
Every declared range and what it resolved to', + '', + readTail(process.env.RESOLVED_FILE, MAX_RESOLVED_CHARS), + '', + '
', + '', + '## Output', + '', + '```', + readTail(process.env.LOG_FILE, MAX_LOG_CHARS), + '```', + '', + `Updated automatically by \`floating_deps_check.yml\`. Edits to this body are overwritten on the next failure.`, + ].join('\n'); +} + +function ensureLabel() { + try { + gh([`repos/${repo}/labels/${encodeURIComponent(LABEL)}`]); + } catch (e) { + gh([ + `repos/${repo}/labels`, + '-X', + 'POST', + '-f', + `name=${LABEL}`, + '-f', + 'color=D93F0B', + '-f', + 'description=Raised by the scheduled floating dependency check', + ]); + } +} + +function findOpenIssue() { + // The filters go in the query string. Passing them with -f makes gh send a request body, + // which turns this GET into a write attempt against the issues endpoint. + const query = `state=open&labels=${encodeURIComponent(LABEL)}`; + const found = gh([ + `repos/${repo}/issues?${query}`, + '--paginate', + '--jq', + `[.[] | select(.pull_request == null) | select(.body | contains("${MARKER}")) | .number] | first // empty`, + ]).trim(); + return found || null; +} + +function report() { + ensureLabel(); + const body = buildBody(); + const existing = findOpenIssue(); + if (existing) { + gh([`repos/${repo}/issues/${existing}`, '-X', 'PATCH', '-f', `body=${body}`]); + console.log(`updated issue #${existing}`); + } else { + const created = gh([ + `repos/${repo}/issues`, + '-X', + 'POST', + '-f', + `title=${TITLE}`, + '-f', + `body=${body}`, + '-f', + `labels[]=${LABEL}`, + '--jq', + '.number', + ]).trim(); + console.log(`opened issue #${created}`); + } +} + +/** + * Closes the issue a previous failure opened. Only an issue carrying this workflow's marker is + * touched, so a run passing can never close something a person filed. + */ +function resolveIssue() { + const existing = findOpenIssue(); + if (!existing) { + console.log('no open issue to close'); + return; + } + const runUrl = `${process.env.GITHUB_SERVER_URL}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + gh([ + `repos/${repo}/issues/${existing}/comments`, + '-X', + 'POST', + '-f', + `body=The floating dependency check passed on \`${process.env.GITHUB_SHA}\`. Closing.\n\nRun: ${runUrl}`, + ]); + gh([`repos/${repo}/issues/${existing}`, '-X', 'PATCH', '-f', 'state=closed']); + console.log(`closed issue #${existing}`); +} + +function gh(args) { + return execFileSync('gh', ['api', ...args], { + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }); +} diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 787ea23e8f0..6f2bc5dda5b 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -13,7 +13,7 @@ jobs: sweep: runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: days-before-stale: 30 days-before-close: 3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bf04cc73ba..6fd52590acb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -274,7 +274,7 @@ After implementing your changes, stage the changes and commit them. Refer to the ``` git add . -git commit -m "type(scope): Message in present tense" +git commit -m "type(scope): message in present tense" ``` ### Creating a pull request @@ -354,8 +354,12 @@ myNewApi: number; This repo uses [Conventional Commits](https://www.conventionalcommits.org). ``` -type(scope): Message in present tense +type(scope): message in present tense ``` + +The subject must not start with a capital letter: `@commitlint/config-conventional` rejects sentence-case, +start-case, pascal-case and upper-case subjects. + `type` may be one of: * **feat** (new feature) * **fix** (bug fix) @@ -380,7 +384,7 @@ type(scope): Message in present tense If a commit affects more than one package, separate them with a comma: ``` -fix(core,common): Fix the thing +fix(core,common): fix the thing ``` If a commit applies to no particular package (e.g. a tooling change in the root package.json), the scope can be omitted. @@ -394,7 +398,7 @@ Please also make your pull request against the `major` branch rather than `maste Example: ``` -feat(core): Add new field to Customer +feat(core): add new field to Customer Relates to #123. This commit adds the "foo" field to the Custom entity. diff --git a/bun.lock b/bun.lock index 42685f766b8..ec08d8f9709 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "vendure", "dependencies": { "@types/nodemailer": "^8.0.1", - "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^5.54.1", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", @@ -16,7 +16,7 @@ }, "devDependencies": { "@commitlint/cli": "^19.1.0", - "@commitlint/config-conventional": "^19.1.0", + "@commitlint/config-conventional": "^21.2.2", "@gql.tada/cli-utils": "^1.6.5", "@graphql-codegen/add": "6.0.0", "@graphql-codegen/cli": "6.0.0", @@ -59,7 +59,7 @@ }, "packages/admin-ui": { "name": "@vendure/admin-ui", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@angular/animations": "^19.2.4", "@angular/cdk": "^19.2.7", @@ -81,7 +81,7 @@ "@ng-select/ng-select": "^14.2.6", "@ngx-translate/core": "^16.0.4", "@ngx-translate/http-loader": "^18.0.0", - "@vendure/common": "3.7.2", + "@vendure/common": "3.7.3", "@webcomponents/custom-elements": "^1.6.0", "apollo-angular": "^10.0.3", "apollo-upload-client": "^18.0.1", @@ -124,7 +124,7 @@ "@types/jasminewd2": "~2.0.10", "@types/node": "^18.17.9", "@types/react": "^19.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^5.59.2", "@vendure/ngx-translate-extract": "^9.4.2", "cross-spawn": "^7.0.3", @@ -148,7 +148,7 @@ }, "packages/admin-ui-plugin": { "name": "@vendure/admin-ui-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "date-fns": "^4.0.0", "express-rate-limit": "^7.5.0", @@ -167,7 +167,7 @@ }, "packages/asset-server-plugin": { "name": "@vendure/asset-server-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@types/express": "^5.0.1", "express": "^5.1.0", @@ -189,7 +189,7 @@ }, "packages/cli": { "name": "@vendure/cli", - "version": "3.7.2", + "version": "3.7.3", "bin": { "vendure": "dist/cli.js", }, @@ -218,7 +218,7 @@ }, "packages/common": { "name": "@vendure/common", - "version": "3.7.2", + "version": "3.7.3", "devDependencies": { "rimraf": "^5.0.5", "typescript": "5.8.2", @@ -226,10 +226,12 @@ }, "packages/core": { "name": "@vendure/core", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@apollo/server": "^4.12.2", + "@apollographql/graphql-playground-html": "^1.6.29", "@graphql-tools/stitch": "^10.1.16", + "@graphql-tools/utils": "^11.0.0", "@nestjs/apollo": "~13.1.0", "@nestjs/common": "^11.0.12", "@nestjs/core": "^11.0.12", @@ -237,7 +239,7 @@ "@nestjs/platform-express": "^11.0.12", "@nestjs/typeorm": "^11.0.0", "@types/express": "^5.0.1", - "@vendure/common": "3.7.2", + "@vendure/common": "3.7.3", "bcrypt": "^6.0.0", "cookie-session": "^2.1.0", "cron-time-generator": "^2.0.3", @@ -299,18 +301,18 @@ }, "packages/create": { "name": "@vendure/create", - "version": "3.7.2", + "version": "3.7.3", "bin": { "create": "./index.js", }, "dependencies": { "@clack/prompts": "^0.11.0", - "@vendure/common": "3.6.4", + "@vendure/common": "3.7.2", "commander": "^14.0.1", "cross-spawn": "^7.0.6", "fs-extra": "^11.2.0", "handlebars": "^4.7.9", - "open": "^10.2.0", + "open": "^11.0.1", "picocolors": "^1.1.1", "semver": "^7.7.2", "tar": "^7.4.3", @@ -320,7 +322,7 @@ "@types/fs-extra": "^11.0.4", "@types/handlebars": "^4.1.0", "@types/semver": "^7.5.8", - "@vendure/core": "3.6.4", + "@vendure/core": "3.7.2", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "typescript": "5.8.2", @@ -329,7 +331,7 @@ }, "packages/dashboard": { "name": "@vendure/dashboard", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@babel/core": "^7.29.0", "@babel/preset-react": "^7.28.5", @@ -381,7 +383,7 @@ "graphql": "^16.11.0", "input-otp": "^1.4.2", "json-edit-react": "^1.23.1", - "lucide-react": "^0.475.0", + "lucide-react": "^0.577.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react": "^19.2.4", @@ -408,8 +410,8 @@ "@storybook/addon-vitest": "^10.3.1", "@storybook/react-vite": "^10.3.1", "@types/node": "^22.19.0", - "@vendure/common": "3.7.2", - "@vendure/core": "3.7.2", + "@vendure/common": "3.7.3", + "@vendure/core": "3.7.3", "@vitest/browser": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.39.0", @@ -429,7 +431,7 @@ }, "packages/dev-server": { "name": "dev-server", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@nestjs/axios": "^4.0.0", "@vendure/asset-server-plugin": "3.7.1", @@ -456,7 +458,7 @@ }, "packages/email-plugin": { "name": "@vendure/email-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@types/nodemailer": "^8.0.1", "dateformat": "^3.0.3", @@ -479,7 +481,7 @@ }, "packages/graphiql-plugin": { "name": "@vendure/graphiql-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "express": "^5.1.0", }, @@ -501,7 +503,7 @@ }, "packages/harden-plugin": { "name": "@vendure/harden-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "graphql-query-complexity": "^0.12.0", }, @@ -515,7 +517,7 @@ }, "packages/job-queue-plugin": { "name": "@vendure/job-queue-plugin", - "version": "3.7.2", + "version": "3.7.3", "devDependencies": { "@vendure/common": "3.6.3", "@vendure/core": "3.6.3", @@ -535,7 +537,7 @@ }, "packages/telemetry-plugin": { "name": "@vendure/telemetry-plugin", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", @@ -562,7 +564,7 @@ }, "packages/testing": { "name": "@vendure/testing", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@vendure/common": "3.7.2", @@ -586,7 +588,7 @@ }, "packages/ui-devkit": { "name": "@vendure/ui-devkit", - "version": "3.7.2", + "version": "3.7.3", "dependencies": { "@angular-devkit/build-angular": "^19.2.5", "@angular/cli": "^19.2.5", @@ -1055,7 +1057,7 @@ "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], - "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], + "@commitlint/config-conventional": ["@commitlint/config-conventional@21.2.2", "", { "dependencies": { "@commitlint/types": "^21.2.0", "conventional-changelog-conventionalcommits": "^10.0.0" } }, "sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ=="], "@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="], @@ -1087,6 +1089,8 @@ "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + "@conventional-changelog/template": ["@conventional-changelog/template@1.4.0", "", {}, "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], @@ -1315,7 +1319,7 @@ "@graphql-tools/url-loader": ["@graphql-tools/url-loader@8.0.33", "", { "dependencies": { "@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-http": "^1.1.9", "@graphql-tools/executor-legacy-ws": "^1.1.19", "@graphql-tools/utils": "^10.9.1", "@graphql-tools/wrap": "^10.0.16", "@types/ws": "^8.0.0", "@whatwg-node/fetch": "^0.10.0", "@whatwg-node/promise-helpers": "^1.0.0", "isomorphic-ws": "^5.0.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0", "ws": "^8.17.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw=="], - "@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-tools/wrap": ["@graphql-tools/wrap@11.1.21", "", { "dependencies": { "@graphql-tools/delegate": "^12.1.1", "@graphql-tools/schema": "^10.0.29", "@graphql-tools/utils": "^11.0.0", "@whatwg-node/promise-helpers": "^1.3.2", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-28a+cTtDONeO+Bg+241biALEHdZuIyFk7libduztuh+Ecwk5hCZgLVFn1S5yJXgvMvkm9+MRVSRQaWILsyougA=="], @@ -1983,7 +1987,7 @@ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], @@ -2029,6 +2033,8 @@ "@sigstore/verify": ["@sigstore/verify@3.1.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag=="], + "@simple-libs/stream-utils": ["@simple-libs/stream-utils@2.0.0", "", {}, "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.47", "", {}, "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], @@ -2583,25 +2589,25 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.69.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/type-utils": "8.69.0", "@typescript-eslint/utils": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.69.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.69.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.69.0", "@typescript-eslint/types": "^8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0" } }, "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.69.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.62.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA=="], "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.69.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260515.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260515.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260515.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260515.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260515.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260515.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260515.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260515.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-dWhzJGb9iJfCDsjz/LzPkbMj5XDbFWyZmDyTixcmQPgX5zFOPZ+sAdlCpTZFGEJhA7OqXG+lZyajbJfjYvQorA=="], @@ -2823,6 +2829,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "argue-cli": ["argue-cli@3.1.0", "", {}, "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -3109,7 +3117,7 @@ "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="], + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@10.4.0", "", { "dependencies": { "@conventional-changelog/template": "^1.4.0" } }, "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA=="], "conventional-changelog-core": ["conventional-changelog-core@7.0.0", "", { "dependencies": { "@hutson/parse-repository-url": "^5.0.0", "add-stream": "^1.0.0", "conventional-changelog-writer": "^7.0.0", "conventional-commits-parser": "^5.0.0", "git-raw-commits": "^4.0.0", "git-semver-tags": "^7.0.0", "hosted-git-info": "^7.0.0", "normalize-package-data": "^6.0.0", "read-pkg": "^8.0.0", "read-pkg-up": "^10.0.0" } }, "sha512-UYgaB1F/COt7VFjlYKVE/9tTzfU3VUq47r6iWf6lM5T7TlOxr0thI63ojQueRLIpVbrtHK4Ffw+yQGduw2Bhdg=="], @@ -3459,9 +3467,9 @@ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], - "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.4", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw=="], + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.6", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-uZnh24On2bk478AkaDAKcpRiYhS3qFSaSNvpXxV6/Ek/BBDICkWGG7MnBSfjnIlCVBei5vsLlIeQYxbF22+Udg=="], - "eslint-plugin-storybook": ["eslint-plugin-storybook@10.5.10", "", { "dependencies": { "@typescript-eslint/types": "^8.60.0", "@typescript-eslint/utils": "^8.60.0" }, "peerDependencies": { "eslint": ">=8" } }, "sha512-NeOu3axmhNZfRuHRciFH0a/OVgvtAJLRHAwiJjcGkPAcElNmwEPp+pqoiwxytx7kHInz7mVVoBfPwVc8kPmOLw=="], + "eslint-plugin-storybook": ["eslint-plugin-storybook@10.6.0", "", { "dependencies": { "@typescript-eslint/types": "^8.60.0", "@typescript-eslint/utils": "^8.60.0" }, "peerDependencies": { "eslint": ">=8" } }, "sha512-HK3yD5Nslv97buJ5JfT1xFkTOoyJgGIENmBoZZI3F8t2UpBzb/dHpKyCh5bQ8AvJWnhoM/WAtDNqyd9nq6xgcg=="], "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], @@ -3791,7 +3799,7 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], @@ -4227,7 +4235,7 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], - "lucide-react": ["lucide-react@0.475.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="], + "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], @@ -4433,8 +4441,6 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="], - "needle": ["needle@3.3.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": "bin/needle" }, "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -4547,7 +4553,7 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "open": ["open@11.0.1", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.0", "wsl-utils": "^1.0.0" } }, "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw=="], "opencollective-postinstall": ["opencollective-postinstall@2.0.3", "", { "bin": "index.js" }, "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="], @@ -4781,7 +4787,7 @@ "posthtml-render": ["posthtml-render@3.0.0", "", { "dependencies": { "is-json": "^2.0.1" } }, "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": "bin.js" }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -5373,7 +5379,7 @@ "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], @@ -5607,7 +5613,7 @@ "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -5667,6 +5673,8 @@ "@angular-devkit/core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@angular-devkit/core/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "@angular-devkit/core/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], @@ -5675,13 +5683,7 @@ "@angular-devkit/schematics/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], - "@angular-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], - - "@angular-eslint/schematics/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@angular-eslint/eslint-plugin-template/@typescript-eslint/types": ["@typescript-eslint/types@8.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], "@angular-eslint/schematics/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -5689,8 +5691,6 @@ "@angular-eslint/template-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - "@angular-eslint/utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], - "@angular/build/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], "@angular/build/@inquirer/confirm": ["@inquirer/confirm@5.1.6", "", { "dependencies": { "@inquirer/core": "^10.1.7", "@inquirer/type": "^3.0.4" }, "peerDependencies": { "@types/node": ">=18" } }, "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw=="], @@ -5795,6 +5795,8 @@ "@clr/core/ramda": ["ramda@0.27.2", "", {}, "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA=="], + "@commitlint/config-conventional/@commitlint/types": ["@commitlint/types@21.2.0", "", { "dependencies": { "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" } }, "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw=="], + "@commitlint/config-validator/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -5813,6 +5815,8 @@ "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "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" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + "@dotenvx/dotenvx/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@dotenvx/dotenvx/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], @@ -5821,6 +5825,8 @@ "@eslint/eslintrc/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -5831,22 +5837,34 @@ "@graphql-codegen/add/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + "@graphql-codegen/cli/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/client-preset/@graphql-codegen/typescript": ["@graphql-codegen/typescript@5.0.7", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^6.1.0", "@graphql-codegen/schema-ast": "^5.0.0", "@graphql-codegen/visitor-plugin-common": "6.2.2", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-kZwcu9Iat5RWXxLGPnDbG6qVbGTigF25/aGqCG/DCQ1Al8RufSjVXhIOkJBp7QWAqXn3AupHXL1WTMXP7xs4dQ=="], "@graphql-codegen/client-preset/@graphql-codegen/typescript-operations": ["@graphql-codegen/typescript-operations@5.0.7", "", { "dependencies": { "@graphql-codegen/plugin-helpers": "^6.1.0", "@graphql-codegen/typescript": "^5.0.7", "@graphql-codegen/visitor-plugin-common": "6.2.2", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-sock": "^1.0.0" }, "optionalPeers": ["graphql-sock"] }, "sha512-5N3myNse1putRQlp8+l1k9ayvc98oq2mPJx0zN8MTOlTBxcb2grVPFRLy5wJJjuv9NffpyCkVJ9LvUaf8mqQgg=="], + "@graphql-codegen/client-preset/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/client-preset/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], "@graphql-codegen/core/@graphql-tools/schema": ["@graphql-tools/schema@10.0.31", "", { "dependencies": { "@graphql-tools/merge": "^9.1.7", "@graphql-tools/utils": "^11.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ=="], + "@graphql-codegen/core/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/core/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], "@graphql-codegen/fragment-matcher/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + "@graphql-codegen/gql-tag-operations/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/gql-tag-operations/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + "@graphql-codegen/plugin-helpers/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/plugin-helpers/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + "@graphql-codegen/schema-ast/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/schema-ast/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], "@graphql-codegen/typed-document-node/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], @@ -5859,55 +5877,37 @@ "@graphql-codegen/typescript-operations/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + "@graphql-codegen/visitor-plugin-common/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/visitor-plugin-common/graphql-tag": ["graphql-tag@2.12.6", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg=="], "@graphql-codegen/visitor-plugin-common/tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], - "@graphql-tools/apollo-engine-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/batch-delegate/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/batch-execute/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/code-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/delegate/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], + "@graphql-tools/executor-common/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], "@graphql-tools/executor-graphql-ws/@graphql-tools/executor-common": ["@graphql-tools/executor-common@0.0.6", "", { "dependencies": { "@envelop/core": "^5.3.0", "@graphql-tools/utils": "^10.9.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow=="], + "@graphql-tools/executor-graphql-ws/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-tools/executor-graphql-ws/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@graphql-tools/executor-legacy-ws/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], + "@graphql-tools/executor-http/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], "@graphql-tools/executor-legacy-ws/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@graphql-tools/git-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], + "@graphql-tools/github-loader/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], "@graphql-tools/github-loader/sync-fetch": ["sync-fetch@0.6.0-2", "", { "dependencies": { "node-fetch": "^3.3.2", "timeout-signal": "^2.0.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A=="], - "@graphql-tools/graphql-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/graphql-tag-pluck/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], - "@graphql-tools/graphql-tag-pluck/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/import/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - - "@graphql-tools/json-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/load/@graphql-tools/schema": ["@graphql-tools/schema@10.0.31", "", { "dependencies": { "@graphql-tools/merge": "^9.1.7", "@graphql-tools/utils": "^11.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ=="], - "@graphql-tools/load/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], - "@graphql-tools/relay-operation-optimizer/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], - "@graphql-tools/stitch/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], + "@graphql-tools/url-loader/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], "@graphql-tools/url-loader/@graphql-tools/wrap": ["@graphql-tools/wrap@10.1.4", "", { "dependencies": { "@graphql-tools/delegate": "^10.2.23", "@graphql-tools/schema": "^10.0.25", "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg=="], @@ -5915,8 +5915,6 @@ "@graphql-tools/url-loader/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@graphql-tools/wrap/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -6169,15 +6167,27 @@ "@types/pg-pool/@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], - "@typescript-eslint/eslint-plugin/semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.69.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.69.0", "@typescript-eslint/tsconfig-utils": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w=="], + + "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.69.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.69.0", "@typescript-eslint/tsconfig-utils": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w=="], - "@typescript-eslint/utils/semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@vendure-io/ui/lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="], @@ -6353,6 +6363,8 @@ "eslint/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -6379,9 +6391,7 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint-plugin-storybook/@typescript-eslint/types": ["@typescript-eslint/types@8.67.0", "", {}, "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww=="], - - "eslint-plugin-storybook/@typescript-eslint/utils": ["@typescript-eslint/utils@8.67.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/typescript-estree": "8.67.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A=="], + "eslint-plugin-storybook/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], "express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -6419,6 +6429,8 @@ "global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "gql.tada/@0no-co/graphqlsp": ["@0no-co/graphqlsp@1.17.5", "", { "dependencies": { "@gql.tada/internal": "^1.2.1", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" } }, "sha512-wsygfTO3pdkz4idDw8BxdMdgB9AB8nuJDJTUwYy87BiZEZRXS4bEfn4oNl9VtxTo6j2lzX/gED+IvOgxsSy2Jg=="], "gql.tada/@gql.tada/cli-utils": ["@gql.tada/cli-utils@1.9.3", "", { "dependencies": { "@0no-co/graphqlsp": "^1.17.3", "@gql.tada/internal": "1.2.2", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependencies": { "@gql.tada/svelte-support": "1.0.3", "@gql.tada/vue-support": "1.0.3", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@gql.tada/svelte-support", "@gql.tada/vue-support"] }, "sha512-P1TiXErpJwIi73sei5fzwGA/SOeCaIHFWFR4RdZPLwqxZzQN0T6MAUivzXBgKCORL67rvYnLaaPoWeqWq/61ug=="], @@ -6427,6 +6439,8 @@ "graphql-config/@graphql-tools/merge": ["@graphql-tools/merge@9.1.7", "", { "dependencies": { "@graphql-tools/utils": "^11.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ=="], + "graphql-config/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "graphql-config/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], "graphql-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -6775,8 +6789,6 @@ "nx/flat": ["flat@5.0.2", "", { "bin": "cli.js" }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], - "nx/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "nx/jsonc-parser": ["jsonc-parser@3.2.0", "", {}, "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="], "nx/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -6915,6 +6927,8 @@ "resolve-url-loader/postcss": ["postcss@8.5.2", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA=="], + "rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + "rollup-plugin-typescript2/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], "rollup-plugin-typescript2/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -6975,6 +6989,8 @@ "storybook/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": "bin/esbuild" }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], + "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "storybook/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "streamroller/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -7075,6 +7091,8 @@ "write-pkg/type-fest": ["type-fest@0.4.1", "", {}, "sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw=="], + "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "xss/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "@angular-devkit/build-angular/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -7133,21 +7151,7 @@ "@angular-devkit/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], + "@angular-devkit/core/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "@angular/build/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -7317,6 +7321,8 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@commitlint/config-conventional/@commitlint/types/conventional-commits-parser": ["conventional-commits-parser@7.1.2", "", { "dependencies": { "@simple-libs/stream-utils": "^2.0.0", "argue-cli": "^3.1.0" }, "bin": { "conventional-commits-parser": "./dist/cli/index.js" } }, "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ=="], + "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], @@ -7329,16 +7335,32 @@ "@eslint/eslintrc/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + "@graphql-codegen/client-preset/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/core/@graphql-tools/schema/@graphql-tools/merge": ["@graphql-tools/merge@9.1.7", "", { "dependencies": { "@graphql-tools/utils": "^11.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ=="], "@graphql-codegen/core/@graphql-tools/schema/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-codegen/core/@graphql-tools/schema/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/core/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@graphql-codegen/gql-tag-operations/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@graphql-codegen/plugin-helpers/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@graphql-codegen/schema-ast/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@graphql-codegen/typescript-operations/@graphql-codegen/visitor-plugin-common/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/typescript-operations/@graphql-codegen/visitor-plugin-common/graphql-tag": ["graphql-tag@2.12.6", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg=="], + "@graphql-codegen/typescript/@graphql-codegen/visitor-plugin-common/@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-codegen/typescript/@graphql-codegen/visitor-plugin-common/graphql-tag": ["graphql-tag@2.12.6", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg=="], + "@graphql-codegen/visitor-plugin-common/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/visitor-plugin-common/graphql-tag/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@graphql-tools/github-loader/sync-fetch/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -7561,7 +7583,15 @@ "@types/pg-pool/@types/pg/pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], - "@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "@vendure/admin-ui/@types/node/undici-types": ["undici-types@5.26.5", "", {}, ""], @@ -7581,6 +7611,8 @@ "@vendure/dashboard/eslint/file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, ""], + "@vendure/dashboard/eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@vendure/dashboard/eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "@vendure/graphiql-plugin/rimraf/glob": ["glob@11.1.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": "dist/esm/bin.mjs" }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], @@ -7705,8 +7737,6 @@ "conventional-recommended-bump/meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "copy-webpack-plugin/globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "copy-webpack-plugin/globby/path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], "copy-webpack-plugin/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], @@ -7727,10 +7757,6 @@ "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0" } }, "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.67.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.67.0", "@typescript-eslint/tsconfig-utils": "8.67.0", "@typescript-eslint/types": "8.67.0", "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw=="], - "eslint/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], "express/body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -8103,6 +8129,8 @@ "shadcn/execa/strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "shadcn/open/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "shadcn/ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -8181,6 +8209,8 @@ "storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "streamroller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "streamroller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], @@ -8295,36 +8325,6 @@ "write-json-file/make-dir/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "@angular/build/listr2/cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "@angular/build/listr2/cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -8457,8 +8457,12 @@ "@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "@graphql-codegen/typescript-operations/@graphql-codegen/visitor-plugin-common/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/typescript-operations/@graphql-codegen/visitor-plugin-common/graphql-tag/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/typescript/@graphql-codegen/visitor-plugin-common/@graphql-tools/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@graphql-codegen/typescript/@graphql-codegen/visitor-plugin-common/graphql-tag/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@graphql-tools/url-loader/@graphql-tools/wrap/@graphql-tools/delegate/@graphql-tools/batch-execute": ["@graphql-tools/batch-execute@9.0.19", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA=="], @@ -8545,6 +8549,10 @@ "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], + "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="], + + "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="], + "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -8553,6 +8561,12 @@ "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@vendure/dashboard/eslint/@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@vendure/dashboard/eslint/@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -8585,20 +8599,6 @@ "engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.67.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.67.0", "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.67.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "flat-cache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -8735,24 +8735,6 @@ "webpack-dev-server/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/eslint-plugin-template/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "@angular-eslint/utils/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@angular/build/listr2/cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "@angular/build/listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], @@ -8851,6 +8833,10 @@ "@tanstack/eslint-plugin-query/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@vendure/graphiql-plugin/rimraf/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], "babel-loader/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], @@ -8865,12 +8851,6 @@ "conventional-recommended-bump/meow/read-pkg-up/read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - "find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "import-local/pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -8999,8 +8979,6 @@ "conventional-recommended-bump/meow/read-pkg-up/read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "eslint-plugin-storybook/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "import-local/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/docs/docs/guides/core-concepts/healthchecks/index.mdx b/docs/docs/guides/core-concepts/healthchecks/index.mdx index f7818172dce..75b1affdf4c 100644 --- a/docs/docs/guides/core-concepts/healthchecks/index.mdx +++ b/docs/docs/guides/core-concepts/healthchecks/index.mdx @@ -1,60 +1,32 @@ --- title: "Health Checks" metaTitle: "Health Check Endpoint for Monitoring Vendure" -metaDescription: "How Vendure exposes a /health endpoint for monitoring system dependencies like the database and external services." +metaDescription: "What the Vendure /health endpoint reports and how to use it as a liveness probe." --- -Vendure exposes a **`/health`** endpoint that reports the status of critical system dependencies. -This endpoint is essential for production deployments where load balancers, container orchestrators, -and monitoring tools need to know whether the application is healthy and ready to serve traffic. +Vendure exposes a **`/health`** endpoint on the server. It is a plain REST route which returns +`{ "status": "ok" }` as soon as the application is accepting HTTP requests. -## How it works +The response is unconditional. `/health` does not check the database or any other dependency. It only +tells you that this process is running and serving HTTP, which is what a liveness probe needs. -Each configured health check strategy probes a specific dependency and reports back with a status. -The `/health` endpoint aggregates all strategy results into a single response. +:::warning[Changed in v3.6.0] -A healthy response indicates that all dependencies are reachable and functioning correctly. If any -strategy reports a failure, the endpoint returns an unhealthy status, signaling that the system -may not be able to serve requests reliably. +Before v3.6.0, `/health` ran the strategies configured in `systemOptions.healthChecks` and returned a +503 if any of them failed. Since v3.6.0 those strategies are not executed. The strategy classes and +`HealthCheckRegistryService` will be removed in v4.0.0. Remove any custom strategies and set +`healthChecks: []` to silence the startup warning. See [#4441](https://github.com/vendurehq/vendure/issues/4441) for the reasoning. -## Built-in strategies +::: -Vendure ships with two health check strategies out of the box: +Checking dependencies from the application health endpoint was removed on purpose. If `/health` failed +whenever the database was slow, an orchestrator would restart every instance at once, and restarting the +application cannot fix the database. Monitor dependencies with their own alerting and keep the +application probe shallow. -- **`TypeORMHealthCheckStrategy`** — verifies that the database connection is alive by executing - a simple query. Since the database is the most critical dependency for any Vendure instance, - this strategy is enabled by default. - -- **`HttpHealthCheckStrategy`** — checks the availability of an external HTTP service. This is - useful when your Vendure instance depends on external APIs, such as a payment gateway or a - third-party inventory system. - -## Configuration - -Health checks are configured via the `systemOptions.healthChecks` property in the -[`VendureConfig`](/reference/typescript-api/configuration/vendure-config/). You can enable or disable -individual strategies and configure their parameters to match your infrastructure. - -## Use cases - -The `/health` endpoint serves several important operational purposes: - -- **Load balancer probes** — configure your load balancer to poll `/health` and route traffic - only to healthy instances. -- **Container orchestration** — use the endpoint as a Kubernetes liveness or readiness probe - to automatically restart unhealthy pods or delay traffic until the application is ready. -- **Monitoring dashboards** — integrate with monitoring tools to track uptime and get alerted - when dependencies become unreachable. - -## Custom strategies - -When your application has dependencies beyond the database and HTTP services, you can implement -the `HealthCheckStrategy` interface to create custom checks. For example, a custom strategy might -verify connectivity to a Redis cache, an Elasticsearch cluster, or a message broker. - -Custom strategies are added to the same `systemOptions.healthChecks` configuration array alongside -the built-in strategies, and their results are included in the aggregated `/health` response. +The worker can serve the same unconditional `/health` response. See the +[Using Docker guide](/deployment/using-docker/#healthreadiness-checks) for how to enable it. ## Further reading -- [Production configuration guide](/deployment/production-configuration/) — deploying Vendure with proper health checks and monitoring +- [Production configuration guide](/deployment/production-configuration/) diff --git a/docs/docs/guides/core-concepts/promotions/index.mdx b/docs/docs/guides/core-concepts/promotions/index.mdx index 2ac9c874195..024277ec767 100644 --- a/docs/docs/guides/core-concepts/promotions/index.mdx +++ b/docs/docs/guides/core-concepts/promotions/index.mdx @@ -260,6 +260,10 @@ function lineContainsIds(ids: ID[], line: OrderLine): boolean { export class FreeGiftPromotionPlugin {} ``` +:::note[Promotions which discount nothing are not recorded] +A Promotion is only recorded on `Order.promotions` — and therefore only consumes its `usageLimit` and `perCustomerUsageLimit` — if it actually adjusted an OrderLine or a ShippingLine. The exception is a Promotion with at least one action defining `onActivate`/`onDeactivate`: the free gift action above returns `0` from `execute()` until its side effect has added the gift line, so such a Promotion is kept whatever it discounts. If you write a custom action which delivers its benefit by mutating the Order inside `execute()` and returns `0`, declare a no-op `onActivate` so that the Promotion continues to be recorded. +::: + ### Dependency relationships It is possible to establish dependency relationships between a PromotionAction and one or more PromotionConditions. diff --git a/docs/docs/guides/deployment/using-docker.mdx b/docs/docs/guides/deployment/using-docker.mdx index 5ef5813c91d..1080b145bee 100644 --- a/docs/docs/guides/deployment/using-docker.mdx +++ b/docs/docs/guides/deployment/using-docker.mdx @@ -174,22 +174,13 @@ REQUEST: GET http://localhost:3000/health ```json { - "status": "ok", - "info": { - "database": { - "status": "up" - } - }, - "error": {}, - "details": { - "database": { - "status": "up" - } - } + "status": "ok" } ``` -You can also add your own health checks by creating plugins that make use of the [HealthCheckRegistryService](/reference/typescript-api/health-check/health-check-registry-service/). +The response is unconditional and does not check the database or any other dependency. Since v3.6.0 the +strategies configured in `systemOptions.healthChecks` are no longer executed. See the +[health checks guide](/core-concepts/healthchecks/) for details. ### Worker diff --git a/docs/docs/guides/developer-guide/the-api-layer/index.mdx b/docs/docs/guides/developer-guide/the-api-layer/index.mdx index 3ff70cfdf3a..0bbfa0efcdc 100644 --- a/docs/docs/guides/developer-guide/the-api-layer/index.mdx +++ b/docs/docs/guides/developer-guide/the-api-layer/index.mdx @@ -196,6 +196,51 @@ export const config: VendureConfig = { }; ``` +:::note +Field resolvers — those declared with `@ResolveField()` — only get guards. A globally-registered interceptor or +exception filter runs for queries and mutations, but never for a field resolver. This is deliberate: the other +enhancers would then run once per resolved field, which for a list of products means once per field per product. + +If you need per-field behaviour such as timing or logging, use an +[`apiOptions.apolloServerPlugins`](/reference/typescript-api/configuration/api-options#apolloserverplugins) +plugin with the `willResolveField` hook, which _does_ fire for every field: + +```ts +import { ApolloServerPlugin } from '@apollo/server'; +import { Logger, VendureConfig } from '@vendure/core'; + +const perfPlugin: ApolloServerPlugin = { + async requestDidStart() { + return { + async executionDidStart() { + return { + willResolveField({ info }) { + const start = process.hrtime.bigint(); + return () => { + const ms = Number(process.hrtime.bigint() - start) / 1e6; + if (10 < ms) { + Logger.info(`${info.parentType.name}.${info.fieldName}: ${ms}ms`); + } + }; + }, + }; + }, + }; + }, +}; + +export const config: VendureConfig = { + // ... + apiOptions: { + apolloServerPlugins: [perfPlugin], + }, +}; +``` + +If you want traces rather than log lines, the [`@vendure/telemetry-plugin`](/reference/core-plugins/telemetry-plugin/) +package already emits a span per resolver, field resolvers included. +::: + ### Apollo Server plugins diff --git a/docs/docs/guides/extending-the-dashboard/customizing-pages/customizing-list-pages.mdx b/docs/docs/guides/extending-the-dashboard/customizing-pages/customizing-list-pages.mdx index 64734757635..9235160bab4 100644 --- a/docs/docs/guides/extending-the-dashboard/customizing-pages/customizing-list-pages.mdx +++ b/docs/docs/guides/extending-the-dashboard/customizing-pages/customizing-list-pages.mdx @@ -45,6 +45,8 @@ defineDashboardExtension({ Use [Dev Mode](/extending-the-dashboard/extending-overview/#dev-mode) or the [Extension Targets reference](/extending-the-dashboard/customizing-pages/extension-targets/) to find the `pageId` and `blockId`. +A display component replaces whatever the Dashboard would otherwise render in that column, including the built-in renderers that some columns come with. The `price` and `priceWithTax` columns, for example, are rendered as formatted money by default, and registering a display component for one of them replaces that rendering. + ## Custom table cell components You can define your own custom components to render specific table cells: diff --git a/docs/docs/guides/extending-the-dashboard/localization/index.mdx b/docs/docs/guides/extending-the-dashboard/localization/index.mdx index 8e4c68c1c13..cf1c90bb070 100644 --- a/docs/docs/guides/extending-the-dashboard/localization/index.mdx +++ b/docs/docs/guides/extending-the-dashboard/localization/index.mdx @@ -198,7 +198,7 @@ locale. The interface re-renders with your catalog; any string you left untransl Commit the changed `.po` file(s) (and, for a new language, the `lingui.config.js` and `vite/constants.ts` changes) and open a PR against `master`. A translation change is a `fix`, so title -it accordingly, e.g. `fix(dashboard): Add Ukrainian UI translations`. Translations are always welcome, +it accordingly, e.g. `fix(dashboard): add Ukrainian UI translations`. Translations are always welcome, including partial ones — you don't have to translate every string to contribute. Two CI checks gate a translation PR: **`dashboard i18n sync`** (runs `lingui extract` and fails if the diff --git a/docs/docs/reference/dashboard/list-views/list-page.mdx b/docs/docs/reference/dashboard/list-views/list-page.mdx index dbcf2ec88d0..d0431922369 100644 --- a/docs/docs/reference/dashboard/list-views/list-page.mdx +++ b/docs/docs/reference/dashboard/list-views/list-page.mdx @@ -4,7 +4,7 @@ generated: true --- ## ListPage - + Auto-generates a list page with columns generated based on the provided query document fields. @@ -245,6 +245,11 @@ handling. Allows you to customize how the search term is used in the list query options. For instance, when you want the term to filter on specific fields. +The returned filter is merged with the column & faceted filters, so when the term should +match any of several fields, group those fields in an `_or` block. Setting a page-level +`filterOperator: 'OR'` would instead OR together _every_ top-level condition, including +the column filters. + *Example* ```tsx @@ -254,9 +259,11 @@ For instance, when you want the term to filter on specific fields. listQuery={administratorListDocument} onSearchTermChange={searchTerm => { return { - firstName: { contains: searchTerm }, - lastName: { contains: searchTerm }, - emailAddress: { contains: searchTerm }, + _or: [ + { firstName: { contains: searchTerm } }, + { lastName: { contains: searchTerm } }, + { emailAddress: { contains: searchTerm } }, + ], }; }} /> diff --git a/docs/docs/reference/typescript-api/configuration/runtime-vendure-config.mdx b/docs/docs/reference/typescript-api/configuration/runtime-vendure-config.mdx index 08a872944db..5ec0d814ed1 100644 --- a/docs/docs/reference/typescript-api/configuration/runtime-vendure-config.mdx +++ b/docs/docs/reference/typescript-api/configuration/runtime-vendure-config.mdx @@ -2,7 +2,7 @@ title: "RuntimeVendureConfig" generated: true --- - + This interface represents the VendureConfig object available at run-time, i.e. the user-supplied config values have been merged with the [defaultConfig](/reference/typescript-api/configuration/default-config#defaultconfig) values. diff --git a/docs/docs/reference/typescript-api/configuration/system-options.mdx b/docs/docs/reference/typescript-api/configuration/system-options.mdx index a2ada0b5868..db74f79db29 100644 --- a/docs/docs/reference/typescript-api/configuration/system-options.mdx +++ b/docs/docs/reference/typescript-api/configuration/system-options.mdx @@ -19,10 +19,11 @@ interface SystemOptions { ### healthChecks -HealthCheckStrategy[]`} default={`[TypeORMHealthCheckStrategy]`} since="1.6.0" /> +HealthCheckStrategy[]`} default={`[]`} since="1.6.0" /> -Defines an array of [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) instances which are used by the `/health` endpoint to verify -that any critical systems which the Vendure server depends on are also healthy. +Defines an array of [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) instances. Before v3.6.0 the `/health` endpoint ran these +strategies to verify that critical systems which the Vendure server depends on were healthy. Since v3.6.0 +the strategies are not executed and this option has no effect on the `/health` response. ### errorHandlers ErrorHandlerStrategy[]`} default={`[]`} since="2.2.0" /> diff --git a/docs/docs/reference/typescript-api/configuration/vendure-config.mdx b/docs/docs/reference/typescript-api/configuration/vendure-config.mdx index 7b500ec2cb8..5bb8c50eca9 100644 --- a/docs/docs/reference/typescript-api/configuration/vendure-config.mdx +++ b/docs/docs/reference/typescript-api/configuration/vendure-config.mdx @@ -2,7 +2,7 @@ title: "VendureConfig" generated: true --- - + All possible configuration options are defined by the [`VendureConfig`](https://github.com/vendurehq/vendure/blob/master/packages/core/src/config/vendure-config.ts) interface. diff --git a/docs/docs/reference/typescript-api/default-search-plugin/mysql-search-strategy.mdx b/docs/docs/reference/typescript-api/default-search-plugin/mysql-search-strategy.mdx index f735b5e627f..5aebbc30c83 100644 --- a/docs/docs/reference/typescript-api/default-search-plugin/mysql-search-strategy.mdx +++ b/docs/docs/reference/typescript-api/default-search-plugin/mysql-search-strategy.mdx @@ -2,7 +2,7 @@ title: "MysqlSearchStrategy" generated: true --- - + A weighted fulltext search for MySQL / MariaDB. diff --git a/docs/docs/reference/typescript-api/default-search-plugin/postgres-search-strategy.mdx b/docs/docs/reference/typescript-api/default-search-plugin/postgres-search-strategy.mdx index 44d515d5c9e..fe782833420 100644 --- a/docs/docs/reference/typescript-api/default-search-plugin/postgres-search-strategy.mdx +++ b/docs/docs/reference/typescript-api/default-search-plugin/postgres-search-strategy.mdx @@ -2,7 +2,7 @@ title: "PostgresSearchStrategy" generated: true --- - + A weighted fulltext search for PostgeSQL. diff --git a/docs/docs/reference/typescript-api/default-search-plugin/sqlite-search-strategy.mdx b/docs/docs/reference/typescript-api/default-search-plugin/sqlite-search-strategy.mdx index 3633bb7ba4b..734a3a9f639 100644 --- a/docs/docs/reference/typescript-api/default-search-plugin/sqlite-search-strategy.mdx +++ b/docs/docs/reference/typescript-api/default-search-plugin/sqlite-search-strategy.mdx @@ -2,7 +2,7 @@ title: "SqliteSearchStrategy" generated: true --- - + A rather naive search for SQLite / SQL.js. Rather than proper full-text searching, it uses a weighted `LIKE "%term%"` operator instead. diff --git a/docs/docs/reference/typescript-api/entities/order.mdx b/docs/docs/reference/typescript-api/entities/order.mdx index 13b75c4e259..e0028fa023e 100644 --- a/docs/docs/reference/typescript-api/entities/order.mdx +++ b/docs/docs/reference/typescript-api/entities/order.mdx @@ -160,6 +160,11 @@ Surcharges are arbitrary modifications to the Order total which are neither ProductVariants nor discounts resulting from applied Promotions. For example, one-off discounts based on customer interaction, or surcharges based on payment methods. + +Surcharges are attached and removed with `OrderService.addSurchargeToOrder` and +`OrderService.removeSurchargeFromOrder`. Saving an Order does not detach a +Surcharge which is missing from this array, and repricing the Order does not attach one +which was saved without its `order` property set. ### couponCodes diff --git a/docs/docs/reference/typescript-api/entities/surcharge.mdx b/docs/docs/reference/typescript-api/entities/surcharge.mdx index 427f71af8d0..1ccca876cb0 100644 --- a/docs/docs/reference/typescript-api/entities/surcharge.mdx +++ b/docs/docs/reference/typescript-api/entities/surcharge.mdx @@ -21,7 +21,10 @@ class Surcharge extends VendureEntity { @Column('simple-json') taxLines: TaxLine[]; @Index() - @ManyToOne(type => Order, order => order.surcharges, { onDelete: 'CASCADE' }) + @ManyToOne(type => Order, order => order.surcharges, { + onDelete: 'CASCADE', + orphanedRowAction: 'disable', + }) order: Order; @Index() @ManyToOne(type => OrderModification, orderModification => orderModification.surcharges) diff --git a/docs/docs/reference/typescript-api/health-check/health-check-error.mdx b/docs/docs/reference/typescript-api/health-check/health-check-error.mdx index aedd7075a56..488e7b5ee4a 100644 --- a/docs/docs/reference/typescript-api/health-check/health-check-error.mdx +++ b/docs/docs/reference/typescript-api/health-check/health-check-error.mdx @@ -4,8 +4,8 @@ generated: true --- -Thrown from a health indicator to signal a failed check. The `causes` -payload is forwarded to the `/health` response so callers can inspect +Thrown from a health indicator to signal a failed check. Before v3.6.0 the `causes` +payload was forwarded to the `/health` response so callers could inspect which indicator failed and why. `causes` is intentionally typed as `any` (matching terminus) so handlers can pass through arbitrary upstream error payloads (HTTP responses, DB driver errors, etc.) diff --git a/docs/docs/reference/typescript-api/health-check/health-check-registry-service.mdx b/docs/docs/reference/typescript-api/health-check/health-check-registry-service.mdx index 62b276e9679..6cfd020e95b 100644 --- a/docs/docs/reference/typescript-api/health-check/health-check-registry-service.mdx +++ b/docs/docs/reference/typescript-api/health-check/health-check-registry-service.mdx @@ -2,24 +2,13 @@ title: "HealthCheckRegistryService" generated: true --- - + -This service is used to register health indicator functions to be included in the -health check. Health checks can be used by automated services such as Kubernetes -to determine the state of applications it is running. They are also useful for -administrators to get an overview of the health of all the parts of the -Vendure stack. +This service is used to register health indicator functions which, before v3.6.0, were run by the +`/health` endpoint. Since v3.6.0 registered indicators are not executed, and this service will be +removed in v4.0.0. See the [health checks guide](/core-concepts/healthchecks/). -Plugins which rely on external services (web services, databases etc.) can make use of this -service to add a check for that dependency to the Vendure health check. - - -Since v1.6.0, the preferred way to implement a custom health check is by creating a new [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) -and then passing it to the `systemOptions.healthChecks` array. -See the [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) docs for an example configuration. - -The alternative way to register a health check is by injecting this service directly into your -plugin module. To use it in your plugin, you'll need to import the [PluginCommonModule](/reference/typescript-api/plugin/plugin-common-module#plugincommonmodule): +The example below shows how a plugin registered an indicator before v3.6.0: *Example* @@ -50,9 +39,7 @@ class HealthCheckRegistryService { HealthIndicatorFunction | HealthIndicatorFunction[]) => `} /> -Registers one or more [HealthIndicatorFunction](/reference/typescript-api/health-check/health-indicator-function#healthindicatorfunction)s to be added to the -health check endpoint. The indicator will also appear in the Admin UI's -"system status" view. +Registers one or more [HealthIndicatorFunction](/reference/typescript-api/health-check/health-indicator-function#healthindicatorfunction)s. Since v3.6.0 the registered functions are never called. diff --git a/docs/docs/reference/typescript-api/health-check/health-check-strategy.mdx b/docs/docs/reference/typescript-api/health-check/health-check-strategy.mdx index 942ab5e3401..b61edc56f71 100644 --- a/docs/docs/reference/typescript-api/health-check/health-check-strategy.mdx +++ b/docs/docs/reference/typescript-api/health-check/health-check-strategy.mdx @@ -2,25 +2,13 @@ title: "HealthCheckStrategy" generated: true --- - + -This strategy defines health checks which are included as part of the -`/health` endpoint. They should only be used to monitor _critical_ systems -on which proper functioning of the Vendure server depends. +This strategy defines health checks which, before v3.6.0, were run by the `/health` endpoint. +Since v3.6.0 the strategies configured in `systemOptions.healthChecks` are not executed, and this +interface will be removed in v4.0.0. See the [health checks guide](/core-concepts/healthchecks/). -Custom strategies should be added to the `systemOptions.healthChecks` array. -By default, Vendure includes the `TypeORMHealthCheckStrategy`, so if you set the value of the `healthChecks` -array, be sure to include it manually. - -Vendure also ships with the [HttpHealthCheckStrategy](/reference/typescript-api/health-check/http-health-check-strategy#httphealthcheckstrategy), which is convenient -for adding a health check dependent on an HTTP ping. - -:::info - -This is configured via the `systemOptions.healthChecks` property of -your VendureConfig. - -::: +The example below shows how strategies were configured before v3.6.0. *Example* @@ -56,7 +44,7 @@ interface HealthCheckStrategy extends InjectableStrategy { HealthIndicatorFunction`} /> Should return a [HealthIndicatorFunction](/reference/typescript-api/health-check/health-indicator-function#healthindicatorfunction) which performs the check -and resolves to a status payload. +and resolves to a status payload. Since v3.6.0 this method is never called. diff --git a/docs/docs/reference/typescript-api/health-check/http-health-check-strategy.mdx b/docs/docs/reference/typescript-api/health-check/http-health-check-strategy.mdx index 31c18aeec90..7253d76eaa2 100644 --- a/docs/docs/reference/typescript-api/health-check/http-health-check-strategy.mdx +++ b/docs/docs/reference/typescript-api/health-check/http-health-check-strategy.mdx @@ -2,9 +2,10 @@ title: "HttpHealthCheckStrategy" generated: true --- - + -A [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) used to check health by pinging a url. +A [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) used to check health by pinging a url. Since v3.6.0 it is not +executed and will be removed in v4.0.0. The example below shows the pre-v3.6.0 configuration: *Example* diff --git a/docs/docs/reference/typescript-api/health-check/type-ormhealth-check-strategy.mdx b/docs/docs/reference/typescript-api/health-check/type-ormhealth-check-strategy.mdx index 9d3dc0139f2..fb7cf44ef32 100644 --- a/docs/docs/reference/typescript-api/health-check/type-ormhealth-check-strategy.mdx +++ b/docs/docs/reference/typescript-api/health-check/type-ormhealth-check-strategy.mdx @@ -2,11 +2,10 @@ title: "TypeORMHealthCheckStrategy" generated: true --- - + -A [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) used to check the health of the database. This health -check is included by default, but can be customized by explicitly adding it to the -`systemOptions.healthChecks` array: +A [HealthCheckStrategy](/reference/typescript-api/health-check/health-check-strategy#healthcheckstrategy) used to check the health of the database. Since v3.6.0 it is not +executed and will be removed in v4.0.0. The example below shows the pre-v3.6.0 configuration: *Example* @@ -18,8 +17,6 @@ export const config = { systemOptions: { healthChecks:[ // The default key is "database" and the default timeout is 1000ms - // Sometimes this is too short and leads to false negatives in the - // /health endpoint. new TypeORMHealthCheckStrategy({ key: 'postgres-db', timeout: 5000 }), ] } diff --git a/docs/docs/reference/typescript-api/migration/generate-migration.mdx b/docs/docs/reference/typescript-api/migration/generate-migration.mdx index e4872db590c..215443a7611 100644 --- a/docs/docs/reference/typescript-api/migration/generate-migration.mdx +++ b/docs/docs/reference/typescript-api/migration/generate-migration.mdx @@ -2,7 +2,7 @@ title: "GenerateMigration" generated: true --- - + Generates a new migration file based on any schema changes (e.g. adding or removing CustomFields). See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the diff --git a/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx new file mode 100644 index 00000000000..04fbdd24da0 --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx @@ -0,0 +1,37 @@ +--- +title: "MigrationDiagnostic" +generated: true +--- + + +A condition detected while running migrations which cannot be inferred from the return value +of [runMigrations](/reference/typescript-api/migration/run-migrations#runmigrations). + +```ts title="Signature" +interface MigrationDiagnostic { + code: 'no-migrations-matched' | 'schema-out-of-sync'; + lines: string[]; +} +``` + +
+ +### code + + + +Identifies the condition: + +- `no-migrations-matched`: the configured `migrations` patterns matched no files, but the + database has migrations recorded as applied. The patterns must therefore have stopped + matching, for example because they point at compiled output which has not been built. +- `schema-out-of-sync`: the database schema does not match the current entity + configuration, so a new migration needs to be generated. +### lines + + + +The condition rendered as lines of human-readable text. + + +
diff --git a/docs/docs/reference/typescript-api/migration/migration-options.mdx b/docs/docs/reference/typescript-api/migration/migration-options.mdx index c5c6b270b49..da3df8c9c23 100644 --- a/docs/docs/reference/typescript-api/migration/migration-options.mdx +++ b/docs/docs/reference/typescript-api/migration/migration-options.mdx @@ -2,7 +2,7 @@ title: "MigrationOptions" generated: true --- - + Configuration for generating a new migration script via [generateMigration](/reference/typescript-api/migration/generate-migration#generatemigration). diff --git a/docs/docs/reference/typescript-api/migration/revert-last-migration.mdx b/docs/docs/reference/typescript-api/migration/revert-last-migration.mdx index 348eff3bab8..a53ee557561 100644 --- a/docs/docs/reference/typescript-api/migration/revert-last-migration.mdx +++ b/docs/docs/reference/typescript-api/migration/revert-last-migration.mdx @@ -2,7 +2,7 @@ title: "RevertLastMigration" generated: true --- - + Reverts the last applied database migration. See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the underlying migration mechanism. diff --git a/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx b/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx new file mode 100644 index 00000000000..cc36b4c206a --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx @@ -0,0 +1,27 @@ +--- +title: "RunMigrationsOptions" +generated: true +--- + + +Options for [runMigrations](/reference/typescript-api/migration/run-migrations#runmigrations). + +```ts title="Signature" +interface RunMigrationsOptions { + onDiagnostic?: (diagnostic: MigrationDiagnostic) => void; +} +``` + +
+ +### onDiagnostic + +MigrationDiagnostic) => void`} /> + +Invoked for each [MigrationDiagnostic](/reference/typescript-api/migration/migration-diagnostic#migrationdiagnostic) detected during the run. These conditions are +also printed to the console, unless the `VENDURE_RUNNING_IN_CLI` environment variable is +set, which the Vendure CLI does so that it can render them itself. Passing this callback +does not suppress that output. + + +
diff --git a/docs/docs/reference/typescript-api/migration/run-migrations.mdx b/docs/docs/reference/typescript-api/migration/run-migrations.mdx index 675b7369356..d78b24d2d66 100644 --- a/docs/docs/reference/typescript-api/migration/run-migrations.mdx +++ b/docs/docs/reference/typescript-api/migration/run-migrations.mdx @@ -2,13 +2,13 @@ title: "RunMigrations" generated: true --- - + Runs any pending database migrations. See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the underlying migration mechanism. ```ts title="Signature" -function runMigrations(userConfig: Partial): Promise +function runMigrations(userConfig: Partial, options?: RunMigrationsOptions): Promise ``` Parameters @@ -16,3 +16,7 @@ Parameters VendureConfig>`} /> +### options + +RunMigrationsOptions`} /> + diff --git a/docs/docs/reference/typescript-api/plugin/plugin-utilities.mdx b/docs/docs/reference/typescript-api/plugin/plugin-utilities.mdx index 761f9fa69da..4c4e89d4c3f 100644 --- a/docs/docs/reference/typescript-api/plugin/plugin-utilities.mdx +++ b/docs/docs/reference/typescript-api/plugin/plugin-utilities.mdx @@ -44,7 +44,7 @@ Parameters ## ProxyOptions - + Options to configure proxy middleware via [createProxyHandler](/reference/typescript-api/plugin/plugin-utilities#createproxyhandler). diff --git a/docs/docs/reference/typescript-api/promotions/promotion-action.mdx b/docs/docs/reference/typescript-api/promotions/promotion-action.mdx index fb24e9c1f9d..ad6df34c22e 100644 --- a/docs/docs/reference/typescript-api/promotions/promotion-action.mdx +++ b/docs/docs/reference/typescript-api/promotions/promotion-action.mdx @@ -33,7 +33,7 @@ more information. ## PromotionItemAction - + Represents a PromotionAction which applies to individual [OrderLine](/reference/typescript-api/entities/order-line#orderline)s. @@ -67,7 +67,7 @@ class PromotionItemAction ## PromotionOrderAction - + Represents a PromotionAction which applies to the [Order](/reference/typescript-api/entities/order#order) as a whole. @@ -101,7 +101,7 @@ class PromotionOrderAction ## PromotionShippingAction - + Represents a PromotionAction which applies to the shipping cost of an Order. @@ -363,7 +363,7 @@ the Shipping, i.e. the number should be negative. ## PromotionLineAction - + Represents a PromotionAction which applies to individual [OrderLine](/reference/typescript-api/entities/order-line#orderline)s. The difference from PromotionItemAction is that it applies regardless of the Quantity of the OrderLine. diff --git a/docs/docs/reference/typescript-api/scheduled-tasks/default-scheduler-strategy.mdx b/docs/docs/reference/typescript-api/scheduled-tasks/default-scheduler-strategy.mdx index 5ba9cab16e8..01433ad1965 100644 --- a/docs/docs/reference/typescript-api/scheduled-tasks/default-scheduler-strategy.mdx +++ b/docs/docs/reference/typescript-api/scheduled-tasks/default-scheduler-strategy.mdx @@ -2,7 +2,7 @@ title: "DefaultSchedulerStrategy" generated: true --- - + The default [SchedulerStrategy](/reference/typescript-api/scheduled-tasks/scheduler-strategy#schedulerstrategy) implementation that uses the database to execute scheduled tasks. This strategy is configured when you use the diff --git a/docs/manifest.json b/docs/manifest.json index 1e1f2c40964..f595b00c330 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -49,7 +49,7 @@ "title": "Promotions", "slug": "promotions", "file": "docs/guides/core-concepts/promotions/index.mdx", - "lastModified": "2026-02-12T16:47:03+01:00" + "lastModified": "2026-09-05T13:43:48+03:00" }, { "title": "Assets", @@ -163,7 +163,7 @@ "title": "Healthchecks", "slug": "healthchecks", "file": "docs/guides/core-concepts/healthchecks/index.mdx", - "lastModified": "2026-05-22T15:41:37+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "Auth", @@ -206,7 +206,7 @@ "title": "The API Layer", "slug": "the-api-layer", "file": "docs/guides/developer-guide/the-api-layer/index.mdx", - "lastModified": "2026-02-12T16:47:03+01:00" + "lastModified": "2026-09-09T22:51:50+02:00" }, { "title": "The Service Layer", @@ -549,7 +549,7 @@ "title": "Customizing List Pages", "slug": "customizing-list-pages", "file": "docs/guides/extending-the-dashboard/customizing-pages/customizing-list-pages.mdx", - "lastModified": "2026-06-24T13:43:54+02:00" + "lastModified": "2026-09-10T14:44:13+02:00" }, { "title": "Customizing Detail Pages", @@ -650,7 +650,7 @@ "title": "Localization", "slug": "localization", "file": "docs/guides/extending-the-dashboard/localization/index.mdx", - "lastModified": "2026-07-31T11:47:50+02:00" + "lastModified": "2026-09-08T18:42:12+03:00" }, { "title": "Deployment", @@ -753,7 +753,7 @@ "title": "Using Docker", "slug": "using-docker", "file": "docs/guides/deployment/using-docker.mdx", - "lastModified": "2026-05-27T11:00:24+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "Horizontal Scaling", @@ -1432,7 +1432,7 @@ "title": "RuntimeVendureConfig", "slug": "runtime-vendure-config", "file": "docs/reference/typescript-api/configuration/runtime-vendure-config.mdx", - "lastModified": "2026-09-01T11:20:23+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "SettingsStoreFields", @@ -1450,7 +1450,7 @@ "title": "SystemOptions", "slug": "system-options", "file": "docs/reference/typescript-api/configuration/system-options.mdx", - "lastModified": "2026-09-01T11:20:23+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "TrustProxyOptions", @@ -1462,7 +1462,7 @@ "title": "VendureConfig", "slug": "vendure-config", "file": "docs/reference/typescript-api/configuration/vendure-config.mdx", - "lastModified": "2026-09-01T11:20:23+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -1570,13 +1570,13 @@ "title": "MysqlSearchStrategy", "slug": "mysql-search-strategy", "file": "docs/reference/typescript-api/default-search-plugin/mysql-search-strategy.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-08T15:28:16Z" }, { "title": "PostgresSearchStrategy", "slug": "postgres-search-strategy", "file": "docs/reference/typescript-api/default-search-plugin/postgres-search-strategy.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-08T15:28:16Z" }, { "title": "SearchStrategy", @@ -1588,7 +1588,7 @@ "title": "SqliteSearchStrategy", "slug": "sqlite-search-strategy", "file": "docs/reference/typescript-api/default-search-plugin/sqlite-search-strategy.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-08T15:28:16Z" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -1716,7 +1716,7 @@ "title": "Order", "slug": "order", "file": "docs/reference/typescript-api/entities/order.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-09-09T16:31:13+02:00" }, { "title": "OrderableAsset", @@ -1872,7 +1872,7 @@ "title": "Surcharge", "slug": "surcharge", "file": "docs/reference/typescript-api/entities/surcharge.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-09-09T16:31:13+02:00" }, { "title": "Tag", @@ -2036,19 +2036,19 @@ "title": "HealthCheckError", "slug": "health-check-error", "file": "docs/reference/typescript-api/health-check/health-check-error.mdx", - "lastModified": "2026-05-22T15:41:37+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "HealthCheckRegistryService", "slug": "health-check-registry-service", "file": "docs/reference/typescript-api/health-check/health-check-registry-service.mdx", - "lastModified": "2026-05-22T15:41:37+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "HealthCheckStrategy", "slug": "health-check-strategy", "file": "docs/reference/typescript-api/health-check/health-check-strategy.mdx", - "lastModified": "2026-05-22T15:41:37+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "HealthIndicator", @@ -2078,13 +2078,13 @@ "title": "HttpHealthCheckStrategy", "slug": "http-health-check-strategy", "file": "docs/reference/typescript-api/health-check/http-health-check-strategy.mdx", - "lastModified": "2026-05-22T16:47:46+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" }, { "title": "TypeORMHealthCheckStrategy", "slug": "type-ormhealth-check-strategy", "file": "docs/reference/typescript-api/health-check/type-ormhealth-check-strategy.mdx", - "lastModified": "2026-05-22T15:41:37+02:00" + "lastModified": "2026-09-08T16:41:57+02:00" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -2290,7 +2290,7 @@ "title": "GenerateMigration", "slug": "generate-migration", "file": "docs/reference/typescript-api/migration/generate-migration.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-10T12:56:42+02:00" }, { "title": "MigrateAssetTranslationData", @@ -2304,11 +2304,17 @@ "file": "docs/reference/typescript-api/migration/migrate-product-option-group-data.mdx", "lastModified": "2026-07-10T09:53:10+02:00" }, + { + "title": "MigrationDiagnostic", + "slug": "migration-diagnostic", + "file": "docs/reference/typescript-api/migration/migration-diagnostic.mdx", + "lastModified": "2026-09-10T12:56:42+02:00" + }, { "title": "MigrationOptions", "slug": "migration-options", "file": "docs/reference/typescript-api/migration/migration-options.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-10T12:56:42+02:00" }, { "title": "RescaleOrderLinePromotionAdjustments", @@ -2320,13 +2326,19 @@ "title": "RevertLastMigration", "slug": "revert-last-migration", "file": "docs/reference/typescript-api/migration/revert-last-migration.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-10T12:56:42+02:00" }, { "title": "RunMigrations", "slug": "run-migrations", "file": "docs/reference/typescript-api/migration/run-migrations.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-09-10T12:56:42+02:00" + }, + { + "title": "RunMigrationsOptions", + "slug": "run-migrations-options", + "file": "docs/reference/typescript-api/migration/run-migrations-options.mdx", + "lastModified": "2026-09-10T12:56:42+02:00" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -2622,7 +2634,7 @@ "title": "Plugin Utilities", "slug": "plugin-utilities", "file": "docs/reference/typescript-api/plugin/plugin-utilities.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-09-07T13:32:20Z" }, { "title": "PluginCommonModule", @@ -2716,7 +2728,7 @@ "title": "Promotion Action", "slug": "promotion-action", "file": "docs/reference/typescript-api/promotions/promotion-action.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-09-08T15:27:18Z" }, { "title": "Promotion Condition", @@ -2810,7 +2822,7 @@ "title": "DefaultSchedulerStrategy", "slug": "default-scheduler-strategy", "file": "docs/reference/typescript-api/scheduled-tasks/default-scheduler-strategy.mdx", - "lastModified": "2026-05-04T16:38:19+02:00" + "lastModified": "2026-09-08T07:47:52Z" }, { "title": "ScheduledTask", @@ -4219,7 +4231,7 @@ "title": "ListPage", "slug": "list-page", "file": "docs/reference/dashboard/list-views/list-page.mdx", - "lastModified": "2026-08-10T09:00:52Z" + "lastModified": "2026-09-08T10:38:40Z" }, { "title": "PaginatedListDataTable", diff --git a/docs/package-lock.json b/docs/package-lock.json index 69f98863543..d7d67fef674 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1527,9 +1527,9 @@ } }, "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==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", diff --git a/license/signatures/version1/cla.json b/license/signatures/version1/cla.json index f37bc981c75..3b7fdb8e83a 100644 --- a/license/signatures/version1/cla.json +++ b/license/signatures/version1/cla.json @@ -1383,6 +1383,22 @@ "created_at": "2026-09-05T08:20:32Z", "repoId": 136938012, "pullRequestNo": 5267 + }, + { + "name": "cuyl", + "id": 9765936, + "comment_id": 5571887441, + "created_at": "2026-09-07T14:12:27Z", + "repoId": 136938012, + "pullRequestNo": 5302 + }, + { + "name": "Ismatulla", + "id": 66904522, + "comment_id": 5581100301, + "created_at": "2026-09-08T07:32:30Z", + "repoId": 136938012, + "pullRequestNo": 5313 } ] } \ No newline at end of file diff --git a/package.json b/package.json index f2f05caab89..5066c1d6051 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@commitlint/cli": "^19.1.0", - "@commitlint/config-conventional": "^19.1.0", + "@commitlint/config-conventional": "^21.2.2", "@gql.tada/cli-utils": "^1.6.5", "@graphql-codegen/add": "6.0.0", "@graphql-codegen/cli": "6.0.0", @@ -102,7 +102,7 @@ }, "dependencies": { "@types/nodemailer": "^8.0.1", - "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^5.54.1", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index 776e4d125fa..2cbeb351698 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -96,7 +96,7 @@ "@types/jasminewd2": "~2.0.10", "@types/node": "^18.17.9", "@types/react": "^19.0.12", - "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/eslint-plugin": "^8.68.0", "@typescript-eslint/parser": "^5.59.2", "@vendure/ngx-translate-extract": "^9.4.2", "cross-spawn": "^7.0.3", diff --git a/packages/cli/e2e/migrate-command.e2e-spec.ts b/packages/cli/e2e/migrate-command.e2e-spec.ts index 4a0dddf7559..6581556460b 100644 --- a/packages/cli/e2e/migrate-command.e2e-spec.ts +++ b/packages/cli/e2e/migrate-command.e2e-spec.ts @@ -203,6 +203,9 @@ describe( expect(result.success).toBe(true); expect(result.message).toContain('No pending migrations found'); + // A database with no tables is not "out of sync": every table is pending because + // the project has not been set up yet. + expect(result.hasWarnings).toBe(false); expect(result.migrationsRan).toBeDefined(); expect(result.migrationsRan).toHaveLength(0); }); @@ -226,6 +229,60 @@ describe( expect(runResult.migrationsRan?.length).toBeGreaterThan(0); }); + // #5001 — a `migrations` glob that stopped matching must not be reported as "up to date" + it('should report unmatched migration patterns when migrations have already been applied', async () => { + process.chdir(TEST_PROJECT_DIR); + + // Apply a migration so the `migrations` table is non-empty + const generateResult = await generateMigrationOperation({ + name: 'TestMigration', + outputDir: MIGRATIONS_DIR, + }); + expect(generateResult.success).toBe(true); + expect((await runMigrationsOperation()).migrationsRan?.length).toBeGreaterThan(0); + + // Simulate the glob resolving to nothing (wrong cwd, or unbuilt `dist/migrations/*.js`) + await fs.emptyDir(MIGRATIONS_DIR); + + const result = await runMigrationsOperation(); + + expect(result.migrationsRan).toHaveLength(0); + expect(result.hasWarnings).toBe(true); + expect(result.message).toContain('No migration files matched'); + expect(result.message).not.toContain('No pending migrations found'); + // The fixture config uses `path.join(__dirname, ...)`, and an absolute pattern + // ignores the working directory, so pointing at it would misdirect the user + expect(result.message).not.toContain('resolved against the current directory'); + }); + + // #5001 — a schema built with `synchronize: true` has no migration history, so an + // empty `migrations` table must not be taken to mean an empty database + it('should report schema drift on a database with tables but no migration history', async () => { + process.chdir(TEST_PROJECT_DIR); + + const generateResult = await generateMigrationOperation({ + name: 'TestMigration', + outputDir: MIGRATIONS_DIR, + }); + expect(generateResult.success).toBe(true); + expect((await runMigrationsOperation()).migrationsRan?.length).toBeGreaterThan(0); + + // Leave the tables in place but clear the history, which is what a + // `synchronize: true` database looks like, and drop one table so the schema no + // longer matches the entity configuration. + const Database = (await import('better-sqlite3')).default; + const db = new Database(path.join(TEST_PROJECT_DIR, 'test.db')); + db.exec('DELETE FROM "migrations"'); + db.exec('DROP TABLE "history_entry"'); + db.close(); + await fs.emptyDir(MIGRATIONS_DIR); + + const result = await runMigrationsOperation(); + + expect(result.hasWarnings).toBe(true); + expect(result.message).toContain('does not match your current configuration'); + }); + it('should handle database connection errors gracefully', async () => { process.chdir(TEST_PROJECT_DIR); @@ -252,9 +309,8 @@ describe( }); // Re-import the operation after the mock so that it picks up the mocked helper - const { runMigrationsOperation: runMigrationsWithInvalidDb } = await import( - '../src/commands/migrate/migration-operations' - ); + const { runMigrationsOperation: runMigrationsWithInvalidDb } = + await import('../src/commands/migrate/migration-operations'); const result = await runMigrationsWithInvalidDb(); diff --git a/packages/cli/src/commands/dev/dev.spec.ts b/packages/cli/src/commands/dev/dev.spec.ts index b3ceb50fcad..f260b96a66e 100644 --- a/packages/cli/src/commands/dev/dev.spec.ts +++ b/packages/cli/src/commands/dev/dev.spec.ts @@ -1,7 +1,9 @@ +import { ChildProcess } from 'node:child_process'; +import { EventEmitter } from 'node:events'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { discoverDashboardExtensionDirectories, @@ -10,6 +12,7 @@ import { normalizeDevTarget, resolveVendureProjectDirectory, shouldRestartOnFileChange, + startSupervisedDevProcess, waitForDevProcesses, } from './dev'; @@ -21,6 +24,23 @@ function writePackageJson(dir: string, packageJson: Record) { writeFileSync(path.join(dir, 'package.json'), JSON.stringify(packageJson, null, 2)); } +// Stands in for a spawned child process, so that a test can decide exactly how and when it exits. +class FakeChildProcess extends EventEmitter { + exitCode: number | null = null; + signalCode: NodeJS.Signals | null = null; + + kill(signal: NodeJS.Signals): boolean { + this.close(null, signal); + return true; + } + + close(code: number | null, signal: NodeJS.Signals | null) { + this.exitCode = code; + this.signalCode = signal; + this.emit('close', code, signal); + } +} + describe('dev command', () => { describe('getDevProcessDefinitions()', () => { it('uses default entrypoints', () => { @@ -183,12 +203,14 @@ describe('dev command', () => { const dashboardDir = path.join(projectDir, 'src', 'plugins', 'reviews', 'dashboard'); expect( - shouldRestartOnFileChange(path.join(dashboardDir, 'index.tsx'), projectDir, [dashboardDir]), + shouldRestartOnFileChange(path.join(dashboardDir, 'index.tsx'), projectDir, { + dashboardExtensionDirectories: [dashboardDir], + }), ).toBe(false); expect( - shouldRestartOnFileChange(path.join(dashboardDir, 'components', 'rating.ts'), projectDir, [ - dashboardDir, - ]), + shouldRestartOnFileChange(path.join(dashboardDir, 'components', 'rating.ts'), projectDir, { + dashboardExtensionDirectories: [dashboardDir], + }), ).toBe(false); }); @@ -197,9 +219,9 @@ describe('dev command', () => { const dashboardDir = path.join(projectDir, 'src', 'plugins', 'reviews', 'ui'); expect( - shouldRestartOnFileChange(path.join(dashboardDir, 'components', 'rating.ts'), projectDir, [ - dashboardDir, - ]), + shouldRestartOnFileChange(path.join(dashboardDir, 'components', 'rating.ts'), projectDir, { + dashboardExtensionDirectories: [dashboardDir], + }), ).toBe(false); }); @@ -239,6 +261,22 @@ describe('dev command', () => { ); }); + it('does not restart server or worker processes for paths a caller declares as generated', () => { + const projectDir = path.resolve('/project'); + const generatedDir = path.join(projectDir, 'src', 'gql'); + + expect( + shouldRestartOnFileChange(path.join(generatedDir, 'graphql.ts'), projectDir, { + reloadIgnoredPaths: [generatedDir], + }), + ).toBe(false); + expect( + shouldRestartOnFileChange(path.join(projectDir, 'src', 'vendure-config.ts'), projectDir, { + reloadIgnoredPaths: [generatedDir], + }), + ).toBe(true); + }); + it('restarts server or worker processes for TypeScript source files and env changes only', () => { const projectDir = path.resolve('/project'); @@ -299,6 +337,122 @@ describe('dev command', () => { }); }); + describe('startSupervisedDevProcess()', () => { + const startedProcesses: ManagedDevProcess[] = []; + const temporaryDirectories: string[] = []; + + afterEach(() => { + for (const startedProcess of startedProcesses) { + startedProcess.stop('SIGTERM'); + } + startedProcesses.length = 0; + for (const directory of temporaryDirectories) { + rmSync(directory, { recursive: true, force: true }); + } + temporaryDirectories.length = 0; + }); + + function startSupervisor() { + const projectDir = createTempDir(); + temporaryDirectories.push(projectDir); + mkdirSync(path.join(projectDir, 'src'), { recursive: true }); + const children: FakeChildProcess[] = []; + const spawnChild = vi.fn(() => { + const child = new FakeChildProcess(); + children.push(child); + return child as unknown as ChildProcess; + }); + const supervised = startSupervisedDevProcess( + projectDir, + getDevProcessDefinitions().server, + path.join(projectDir, 'node_modules', '.bin', 'ts-node'), + { prefixOutput: false, reloadIgnoredPaths: [], spawnChild }, + ); + startedProcesses.push(supervised); + return { children, projectDir, spawnChild, supervised }; + } + + it('leaves the other dev processes running when a supervised child crashes', async () => { + const { children, supervised } = startSupervisor(); + const stopDashboard = vi.fn(); + const dashboard = new ManagedDevProcess(stopDashboard); + const promise = waitForDevProcesses([supervised, dashboard]); + + children[0].close(1, null); + + expect(supervised.hasClosed).toBe(false); + expect(stopDashboard).not.toHaveBeenCalled(); + + process.emit('SIGINT'); + dashboard.emitClose(null, 'SIGINT'); + await expect(promise).resolves.toBe(130); + }); + + it('respawns a crashed supervised child on the next relevant file change', async () => { + const { children, projectDir, spawnChild, supervised } = startSupervisor(); + const promise = waitForDevProcesses([supervised]); + const configPath = path.join(projectDir, 'src', 'vendure-config.ts'); + + children[0].close(1, null); + expect(spawnChild).toHaveBeenCalledTimes(1); + + // The save is repeated because chokidar reports nothing for a file that appeared + // before it finished its initial scan of the directory. A single save that loses that + // race is invisible for the rest of the run, however long the test then waits. The + // interval is longer than `reloadDebounceMs` so that a save which did land has time to + // restart the process before the next one arrives. + await vi.waitFor( + () => { + writeFileSync(configPath, `export const config = { revision: ${Date.now()} };`); + expect(spawnChild.mock.calls.length).toBeGreaterThanOrEqual(2); + }, + { timeout: 15000, interval: 300 }, + ); + expect(supervised.hasClosed).toBe(false); + + process.emit('SIGINT'); + await expect(promise).resolves.toBe(130); + }, 20000); + + it('reports the crash exit code when something else ends the run', async () => { + const { children, supervised } = startSupervisor(); + const dashboard = new ManagedDevProcess(vi.fn()); + const promise = waitForDevProcesses([supervised, dashboard]); + + children[0].close(1, null); + // The Dashboard then exits cleanly on its own, which ends the run while the server is + // still crashed. The run must not resolve 0. + dashboard.emitClose(0, null); + + await expect(promise).resolves.toBe(1); + }); + + it('ends the whole run when a supervised child is killed by a signal', async () => { + const { children, supervised } = startSupervisor(); + const stopDashboard = vi.fn(); + const dashboard = new ManagedDevProcess(stopDashboard); + const promise = waitForDevProcesses([supervised, dashboard]); + + children[0].close(null, 'SIGKILL'); + + expect(supervised.hasClosed).toBe(true); + expect(stopDashboard).toHaveBeenCalledWith('SIGTERM'); + + dashboard.emitClose(null, 'SIGTERM'); + await expect(promise).resolves.toBe(1); + }); + + it('ends the whole run when a child closes with neither an exit code nor a signal', async () => { + const { children, supervised } = startSupervisor(); + const promise = waitForDevProcesses([supervised]); + + children[0].close(null, null); + + expect(supervised.hasClosed).toBe(true); + await expect(promise).resolves.toBe(1); + }); + }); + describe('waitForDevProcesses()', () => { it('resolves SIGINT shutdowns with the canonical signal exit code', async () => { const stopFirst = vi.fn(); diff --git a/packages/cli/src/commands/dev/dev.ts b/packages/cli/src/commands/dev/dev.ts index 284cc12b808..e0d9af7306f 100644 --- a/packages/cli/src/commands/dev/dev.ts +++ b/packages/cli/src/commands/dev/dev.ts @@ -35,6 +35,15 @@ export interface DevOptions { inspect?: boolean | string; inspectBrk?: boolean | string; reload?: boolean; + /** + * Paths that hold generated files, such as the `src/gql` directory a Dashboard Vite plugin + * writes GraphQL codegen output into. A change under one of these paths never restarts the + * server or the worker, so codegen triggered by an earlier source change does not cause a + * second restart of its own. + * + * A relative path is resolved against the project directory. + */ + reloadIgnoredPaths?: string[]; } const validTargets: DevTarget[] = ['all', 'server', 'worker', 'dashboard']; @@ -70,6 +79,9 @@ export async function devCommand(targetArg?: string, options: DevOptions = {}): ? (['server', 'worker', 'dashboard'] as const).map(t => devProcessDefinitions[t]) : [devProcessDefinitions[target]]; const prefixOutput = processes.length > 1; + const reloadIgnoredPaths = (options.reloadIgnoredPaths ?? []).map(ignoredPath => + path.resolve(projectDir, ignoredPath), + ); validateProjectFiles(projectDir, processes); @@ -77,6 +89,7 @@ export async function devCommand(targetArg?: string, options: DevOptions = {}): startDevProcess(projectDir, processDefinition, { prefixOutput, reload: options.reload !== false && processDefinition.reloadOnChange, + reloadIgnoredPaths, }), ); return await waitForDevProcesses(children, { @@ -153,7 +166,7 @@ export function resolveVendureProjectDirectory(cwd: string): string { function startDevProcess( projectDir: string, processDefinition: DevProcessDefinition, - options: { prefixOutput: boolean; reload: boolean }, + options: { prefixOutput: boolean; reload: boolean; reloadIgnoredPaths: string[] }, ): ManagedDevProcess { const binPath = resolvePackageBin(processDefinition.packageName, processDefinition.binName, projectDir); return options.reload @@ -203,21 +216,33 @@ function startPlainDevProcess( return runningProcess; } -function startSupervisedDevProcess( +export interface SupervisedDevProcessOptions { + prefixOutput: boolean; + reloadIgnoredPaths: string[]; + // Tests pass a fake child process here, to drive the supervisor without spawning anything. + spawnChild?: typeof spawnDevChild; +} + +export function startSupervisedDevProcess( projectDir: string, processDefinition: DevProcessDefinition, binPath: string, - options: { prefixOutput: boolean }, + options: SupervisedDevProcessOptions, ): ManagedDevProcess { + const spawnChild = options.spawnChild ?? spawnDevChild; let dashboardExtensionDirectories = discoverDashboardExtensionDirectories(projectDir); let child: ChildProcess | undefined; + let crashExitCode: number | undefined; let restartTimer: NodeJS.Timeout | undefined; let restarting = false; let stopping = false; + const reloadPathScope = (): ReloadPathScope => ({ + dashboardExtensionDirectories, + reloadIgnoredPaths: options.reloadIgnoredPaths, + }); const watcher = chokidar.watch(projectDir, { ignoreInitial: true, - ignored: (filePath: string) => - isAlwaysIgnoredReloadPath(filePath, projectDir, dashboardExtensionDirectories), + ignored: (filePath: string) => isAlwaysIgnoredReloadPath(filePath, projectDir, reloadPathScope()), }); const runningProcess = new ManagedDevProcess(signal => { @@ -228,20 +253,50 @@ function startSupervisedDevProcess( closeWatcher(); if (child) { stopChildWithGrace(child, signal, restartShutdownGraceMs); + } else { + // The child crashed, and the close handler left the watcher open so a later file change + // could respawn it. Nothing will respawn it now, so report the process closed, carrying + // the code the child crashed with. Without this, `waitForDevProcesses` waits forever for + // a child that no longer exists. + runningProcess.emitClose(crashExitCode ?? 0, null); } }); const startChild = () => { - child = spawnDevChild(projectDir, processDefinition, binPath, options); - child.once('error', error => runningProcess.emit('error', error)); - child.once('close', (code, signal) => { + crashExitCode = undefined; + const startedChild = spawnChild(projectDir, processDefinition, binPath, options); + child = startedChild; + startedChild.once('error', error => runningProcess.emit('error', error)); + startedChild.once('close', (code, signal) => { + // Node fires `exit` (which is what `isChildRunning` reads) before `close`, so a file + // change between the two can already see this child as not running and start its + // replacement directly, ahead of this handler. `child` will have moved on to that + // replacement by the time this late `close` arrives; without this check, restarting + // below would take it right back off the current child and leave the replacement + // running unwatched, the two of them apparently competing for the same port. + if (child !== startedChild) { + return; + } if (restarting && !stopping) { restarting = false; startChild(); return; } - closeWatcher(); - runningProcess.emitClose(code, signal); + if (!isRecoverableChildExit(code, signal, stopping)) { + closeWatcher(); + runningProcess.emitClose(code, signal); + return; + } + // Leave the other supervised processes running, and leave the watcher open so that the + // next relevant file change respawns this one. Remember the exit code: if the run ends + // before the watcher respawns this process, the run failed. + child = undefined; + crashExitCode = code; + writeDevStatus( + processDefinition, + `The ${processDefinition.target} exited with code ${code}. Waiting for a file change to restart it.`, + options, + ); }); }; @@ -253,22 +308,24 @@ function startSupervisedDevProcess( if (stopping || runningProcess.hasClosed) { return; } + const runningChild = child && isChildRunning(child) ? child : undefined; + const action = runningChild ? 'Restarting' : 'Starting'; writeDevStatus( processDefinition, - `Change detected in ${path.relative(projectDir, changedFile)}. Restarting ${processDefinition.target}...`, + `Change detected in ${path.relative(projectDir, changedFile)}. ${action} the ${processDefinition.target}...`, options, ); dashboardExtensionDirectories = discoverDashboardExtensionDirectories(projectDir); - if (!child || !isChildRunning(child)) { + if (!runningChild) { startChild(); return; } restarting = true; - stopChildWithGrace(child, 'SIGTERM', restartShutdownGraceMs); + stopChildWithGrace(runningChild, 'SIGTERM', restartShutdownGraceMs); }; watcher.on('all', (_event, filePath) => { - if (!shouldRestartOnFileChange(filePath, projectDir, dashboardExtensionDirectories)) { + if (!shouldRestartOnFileChange(filePath, projectDir, reloadPathScope())) { return; } if (restartTimer) { @@ -322,20 +379,38 @@ export function waitForDevProcesses( } } }; - const completeChild = (child: ManagedDevProcess, exitCode: number) => { + const runExitCode = (lastExitCode: number): number => { + // A signal delivered to the whole run decides the exit code, so Ctrl+C reports 130 even + // when a supervised process had crashed and not been respawned. + if (shutdownExitCode) { + return shutdownExitCode; + } + // Otherwise the first child to fail on its own decides the exit code. A supervised + // process that crashed earlier fails the run this way, when another process then exits + // cleanly and ends the run. + if (firstNonZeroExitCode) { + return firstNonZeroExitCode; + } + return shutdownExitCode ?? lastExitCode; + }; + // `isOwnFailure` marks an exit code that the child chose for itself, rather than one it was + // given by the shutdown. Such a code is recorded even once a shutdown is under way. A + // supervised process that crashed earlier reports that crash only when another process ends + // the run, by which point the shutdown has already started. + const completeChild = (child: ManagedDevProcess, exitCode: number, isOwnFailure: boolean) => { if (settledChildren.has(child)) { return; } settledChildren.add(child); remainingChildren--; + if (isOwnFailure && exitCode !== 0 && firstNonZeroExitCode === 0) { + firstNonZeroExitCode = exitCode; + } if (!shutdownRequested) { - if (exitCode !== 0) { - firstNonZeroExitCode = exitCode; - } stopChildren('SIGTERM', exitCode); } if (remainingChildren === 0) { - resolveOnce(firstNonZeroExitCode || (shutdownExitCode ?? exitCode)); + resolveOnce(runExitCode(exitCode)); } }; const handleSigint = () => stopChildren('SIGINT', signalToExitCode('SIGINT')); @@ -347,10 +422,14 @@ export function waitForDevProcesses( for (const child of children) { child.once('error', error => { options.onError?.(error); - completeChild(child, 1); + completeChild(child, 1, !shutdownRequested); }); child.once('close', (code, signal) => { - completeChild(child, code ?? signalToExitCode(signal) ?? (shutdownRequested ? 0 : 1)); + completeChild( + child, + code ?? signalToExitCode(signal) ?? (shutdownRequested ? 0 : 1), + signal === null, + ); }); } }); @@ -410,12 +489,32 @@ function isChildRunning(child: ChildProcess): boolean { return child.exitCode === null && child.signalCode === null; } +// Whether a supervised child that just closed should be left for the watcher to respawn, rather +// than treated as the whole `dev` run ending. Only a non-zero exit code the child chose for itself +// qualifies, such as ts-node failing to compile after an edit. A signal means `kill` or a process +// manager decided the process should stop. A later file change fixes none of the other cases: a +// zero exit, a close carrying neither a code nor a signal, or a shutdown in progress. +export function isRecoverableChildExit( + code: number | null, + signal: NodeJS.Signals | null, + stopping: boolean, +): code is number { + return !stopping && signal === null && code !== null && code !== 0; +} + +export interface ReloadPathScope { + // Vite reloads these on its own. + dashboardExtensionDirectories?: string[]; + // Already resolved to absolute paths by `devCommand`. + reloadIgnoredPaths?: string[]; +} + export function shouldRestartOnFileChange( filePath: string, projectDir: string, - dashboardExtensionDirectories: string[] = [], + scope: ReloadPathScope = {}, ): boolean { - if (isAlwaysIgnoredReloadPath(filePath, projectDir, dashboardExtensionDirectories)) { + if (isAlwaysIgnoredReloadPath(filePath, projectDir, scope)) { return false; } const fileName = path.basename(filePath); @@ -432,11 +531,7 @@ export function shouldRestartOnFileChange( return reloadFileExtensions.has(path.extname(fileName)); } -function isAlwaysIgnoredReloadPath( - filePath: string, - projectDir: string, - dashboardExtensionDirectories: string[], -): boolean { +function isAlwaysIgnoredReloadPath(filePath: string, projectDir: string, scope: ReloadPathScope): boolean { const relativePath = path.relative(projectDir, filePath); if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return true; @@ -446,7 +541,10 @@ function isAlwaysIgnoredReloadPath( if (parts.some(part => reloadIgnoredDirectories.has(part) || part === '__data__')) { return true; } - return dashboardExtensionDirectories.some(dir => isPathInside(filePath, dir)); + if (scope.dashboardExtensionDirectories?.some(dir => isPathInside(filePath, dir))) { + return true; + } + return scope.reloadIgnoredPaths?.some(ignoredPath => isPathInside(filePath, ignoredPath)) ?? false; } export function discoverDashboardExtensionDirectories(projectDir: string): string[] { @@ -480,7 +578,7 @@ function findDashboardMetadataCandidateFiles(projectDir: string): string[] { } for (const entry of entries) { const filePath = path.join(dir, entry.name); - if (isAlwaysIgnoredReloadPath(filePath, projectDir, [])) { + if (isAlwaysIgnoredReloadPath(filePath, projectDir, {})) { continue; } if (entry.isDirectory()) { diff --git a/packages/cli/src/commands/migrate/migrate.ts b/packages/cli/src/commands/migrate/migrate.ts index 407c8d7bfc2..5dfa6b77781 100644 --- a/packages/cli/src/commands/migrate/migrate.ts +++ b/packages/cli/src/commands/migrate/migrate.ts @@ -84,7 +84,12 @@ async function handleNonInteractiveMode(options: MigrateOptions) { return; } if (result.success) { - log.success(result.message); + // A warning is not a success, even though the exit code stays 0. + if (result.hasWarnings) { + log.warn(result.message); + } else { + log.success(result.message); + } } else { log.error(result.message); process.exit(1); diff --git a/packages/cli/src/commands/migrate/migration-operations.ts b/packages/cli/src/commands/migrate/migration-operations.ts index 7aa806e6e84..8218d02e542 100644 --- a/packages/cli/src/commands/migrate/migration-operations.ts +++ b/packages/cli/src/commands/migrate/migration-operations.ts @@ -1,5 +1,11 @@ import { log } from '@clack/prompts'; -import { generateMigration, revertLastMigration, runMigrations, VendureConfig } from '@vendure/core'; +import { + generateMigration, + MigrationDiagnostic, + revertLastMigration, + runMigrations, + VendureConfig, +} from '@vendure/core'; import path from 'node:path'; import { loadVendureConfigFile } from '../../shared/load-vendure-config-file'; @@ -7,6 +13,8 @@ import { validateVendureProjectDirectory } from '../../shared/project-validation import { analyzeProject } from '../../shared/shared-prompts'; import { VendureConfigRef } from '../../shared/vendure-config-ref'; +import { buildMigrationReport } from './migration-report'; + export interface MigrationOptions { name?: string; outputDir?: string; @@ -16,6 +24,8 @@ export interface MigrationOptions { export interface MigrationResult { success: boolean; message: string; + /** The operation completed, but reported something the user needs to act on. */ + hasWarnings?: boolean; migrationName?: string; migrationsRan?: string[]; } @@ -78,15 +88,16 @@ export async function runMigrationsOperation(configFile?: string): Promise diagnostics.push(diagnostic), + }); + const report = buildMigrationReport(migrationsRan, diagnostics); return { success: true, - message: report, + message: report.message, + hasWarnings: report.hasWarnings, migrationsRan, }; } catch (error: any) { diff --git a/packages/cli/src/commands/migrate/migration-report.spec.ts b/packages/cli/src/commands/migrate/migration-report.spec.ts new file mode 100644 index 00000000000..193d22037fb --- /dev/null +++ b/packages/cli/src/commands/migrate/migration-report.spec.ts @@ -0,0 +1,70 @@ +import { MigrationDiagnostic } from '@vendure/core'; +import { describe, expect, it } from 'vitest'; + +import { buildMigrationReport } from './migration-report'; + +const unmatched: MigrationDiagnostic = { + code: 'no-migrations-matched', + lines: ['No migration files matched the configured `migrations` patterns.', ' - dist/migrations/*.js'], +}; +const outOfSync: MigrationDiagnostic = { + code: 'schema-out-of-sync', + lines: [ + 'Your database schema does not match your current configuration.', + ' - ALTER TABLE `product` ADD `foo` varchar(255)', + ], +}; + +describe('buildMigrationReport()', () => { + it('reports the number of migrations that ran', () => { + const report = buildMigrationReport(['1700000000000-first', '1700000000001-second'], []); + + expect(report.summary).toBe('Successfully ran 2 migrations'); + expect(report.message).toBe('Successfully ran 2 migrations'); + expect(report.hasWarnings).toBe(false); + }); + + it('reports no pending migrations when nothing ran and nothing was diagnosed', () => { + const report = buildMigrationReport([], []); + + expect(report.summary).toBe('No pending migrations found'); + expect(report.hasWarnings).toBe(false); + }); + + // #5001 — an empty result caused by unmatched patterns must not read as "up to date" + it('does not claim there are no pending migrations when the patterns matched nothing', () => { + const report = buildMigrationReport([], [unmatched]); + + expect(report.summary).toBe('No migration files could be loaded'); + expect(report.message).not.toContain('No pending migrations found'); + expect(report.details).toContain('dist/migrations/*.js'); + expect(report.hasWarnings).toBe(true); + }); + + it('appends the schema drift warning to the summary', () => { + const report = buildMigrationReport(['1700000000000-first'], [outOfSync]); + + expect(report.summary).toBe('Successfully ran 1 migration'); + expect(report.message).toContain('Successfully ran 1 migration'); + expect(report.details).toContain('does not match your current configuration'); + expect(report.details).toContain('ALTER TABLE `product` ADD `foo` varchar(255)'); + expect(report.hasWarnings).toBe(true); + }); + + // The drift list can be the whole schema, which is more than a terminal should render + it('caps the rendered lines', () => { + const lines = Array.from({ length: 25 }, (_, i) => ` - CREATE TABLE \`t${i}\` (id int)`); + const report = buildMigrationReport([], [{ code: 'schema-out-of-sync', lines }]); + + expect(report.details).toContain('CREATE TABLE `t10` (id int)'); + expect(report.details).not.toContain('CREATE TABLE `t11` (id int)'); + expect(report.details).toContain('...and 14 more'); + }); + + it('renders every diagnostic', () => { + const report = buildMigrationReport([], [unmatched, outOfSync]); + + expect(report.details).toContain('dist/migrations/*.js'); + expect(report.details).toContain('does not match your current configuration'); + }); +}); diff --git a/packages/cli/src/commands/migrate/migration-report.ts b/packages/cli/src/commands/migrate/migration-report.ts new file mode 100644 index 00000000000..4fb6db3e294 --- /dev/null +++ b/packages/cli/src/commands/migrate/migration-report.ts @@ -0,0 +1,50 @@ +import { MigrationDiagnostic } from '@vendure/core'; + +export interface MigrationReport { + /** A one-line summary, suitable for a spinner. */ + summary: string; + /** The diagnostics, rendered. Empty when there are none. */ + details: string; + /** Summary and details together, suitable for a single log line. */ + message: string; + /** True when a diagnostic was reported, so the caller can pick a warning channel. */ + hasWarnings: boolean; +} + +/** + * Builds the report both `vendure migrate --run` and the interactive run-migration command + * print, so the two stay in step. + */ +export function buildMigrationReport( + migrationsRan: string[], + diagnostics: MigrationDiagnostic[], +): MigrationReport { + // "No pending migrations found" is false when the patterns stopped matching: migrations are + // pending, they just could not be loaded. + const patternsUnmatched = diagnostics.some(d => d.code === 'no-migrations-matched'); + const summary = migrationsRan.length + ? `Successfully ran ${migrationsRan.length} migration${migrationsRan.length === 1 ? '' : 's'}` + : patternsUnmatched + ? 'No migration files could be loaded' + : 'No pending migrations found'; + + const details = diagnostics.map(render).join('\n\n'); + return { + summary, + details, + message: details ? `${summary}\n\n${details}` : summary, + hasWarnings: diagnostics.length > 0, + }; +} + +/** + * A schema drift list can be the entire schema, which is more than a terminal should be asked to + * render. Callers of `runMigrations()` still receive every line on the diagnostic. + */ +const maxRenderedLines = 11; + +function render(diagnostic: MigrationDiagnostic): string { + const shown = diagnostic.lines.slice(0, maxRenderedLines); + const remaining = diagnostic.lines.length - shown.length; + return [...shown, ...(remaining ? [` ...and ${remaining} more`] : [])].join('\n'); +} diff --git a/packages/cli/src/commands/migrate/run-migration/run-migration.ts b/packages/cli/src/commands/migrate/run-migration/run-migration.ts index b2c5338962c..50d7b701291 100644 --- a/packages/cli/src/commands/migrate/run-migration/run-migration.ts +++ b/packages/cli/src/commands/migrate/run-migration/run-migration.ts @@ -1,10 +1,11 @@ import { log, spinner } from '@clack/prompts'; -import { runMigrations } from '@vendure/core'; +import { MigrationDiagnostic, runMigrations } from '@vendure/core'; import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command'; import { loadVendureConfigFile } from '../../../shared/load-vendure-config-file'; import { analyzeProject } from '../../../shared/shared-prompts'; import { VendureConfigRef } from '../../../shared/vendure-config-ref'; +import { buildMigrationReport } from '../migration-report'; const cancelledMessage = 'Run migrations cancelled'; @@ -23,11 +24,17 @@ async function runRunMigration(configFile?: string): Promise diagnostics.push(diagnostic), + }); + const report = buildMigrationReport(migrationsRan, diagnostics); + // clack prefixes only the first line, so anything multi-line hangs outside the box. The + // spinner takes the one-line summary and the detail goes to its own log line. + runSpinner.stop(report.summary); + if (report.hasWarnings) { + log.warn(report.details); + } return { project, modifiedSourceFiles: [], diff --git a/packages/core/e2e/asset.e2e-spec.ts b/packages/core/e2e/asset.e2e-spec.ts index 27cf229eba2..2e8562137a7 100644 --- a/packages/core/e2e/asset.e2e-spec.ts +++ b/packages/core/e2e/asset.e2e-spec.ts @@ -1,7 +1,7 @@ import { DeletionResult, LogicalOperator, SortOrder } from '@vendure/common/lib/generated-types'; import { omit } from '@vendure/common/lib/omit'; import { pick } from '@vendure/common/lib/pick'; -import { AssetService, mergeConfig } from '@vendure/core'; +import { Asset, AssetService, ChannelService, mergeConfig, RequestContextService } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import fs from 'fs-extra'; import path from 'node:path'; @@ -613,8 +613,9 @@ describe('Asset resolver', () => { }); }); - // Placed last because creating an asset advances the auto-increment id, which the - // hardcoded-id assertions in the tests above rely on. + // This block and the one after it must stay at the end of the file: each creates an asset, + // which advances the auto-increment id that the total-count and hardcoded-id assertions in + // the tests above rely on. describe('MIME type content validation (GHSA-88rq-mq4v-frmm)', () => { // An SVG that begins with an XML prolog is reported by `file-type` as the generic // `application/xml`, which must not cause the permitted `image/svg+xml` upload to be @@ -638,4 +639,33 @@ describe('Asset resolver', () => { expect(createAssets[0]).not.toHaveProperty('message'); }); }); + + // Guards against the default translation being saved with a null languageCode when no + // RequestContext is passed. https://github.com/vendurehq/vendure/issues/4651 + describe('createFromFileStream without a RequestContext', () => { + const assetGuard: ErrorResultGuard = createErrorResultGuard(input => input.id != null); + + it('creates the default translation with the default Channel languageCode', async () => { + const assetService = server.app.get(AssetService); + const defaultChannel = await server.app.get(ChannelService).getDefaultChannel(); + + const result = await assetService.createFromFileStream( + Readable.from([Buffer.from('%PDF-1.4\n%%EOF\n')]), + 'test-file.pdf', + ); + + assetGuard.assertSuccess(result); + expect(result.name).toBe('test-file.pdf'); + expect(result.translations.map(t => t.languageCode)).toEqual([ + defaultChannel.defaultLanguageCode, + ]); + + // The returned entity carries the Channel ids the service assigned in memory, so it is + // also read back to check what reached the database. Both matter. + expect(result.channels.map(c => c.id)).toEqual([defaultChannel.id]); + const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' }); + const persisted = await assetService.findOne(ctx, result.id, ['channels']); + expect(persisted?.channels.map(c => c.id)).toEqual([defaultChannel.id]); + }); + }); }); diff --git a/packages/core/e2e/custom-field-register-customer.e2e-spec.ts b/packages/core/e2e/custom-field-register-customer.e2e-spec.ts new file mode 100644 index 00000000000..3769badfb43 --- /dev/null +++ b/packages/core/e2e/custom-field-register-customer.e2e-spec.ts @@ -0,0 +1,149 @@ +import { CustomFields, mergeConfig } from '@vendure/core'; +import { createTestEnvironment } from '@vendure/testing'; +import path from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { initialData } from '../../../e2e-common/e2e-initial-data'; +import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; + +import { graphql } from './graphql/graphql-admin'; +import { graphql as graphqlShop } from './graphql/graphql-shop'; + +/** + * These tests need their own config because `custom-fields.e2e-spec.ts` relies on Customer + * having no writable custom fields, and `RegisterCustomerInput` only gains a `customFields` + * field when Customer has at least one which is public and writable. + */ +const customConfig = mergeConfig(testConfig(), { + customFields: { + Customer: [ + { + name: 'validateOnRegister', + type: 'string', + nullable: true, + validate: value => { + if (value !== 'valid') { + return `The value ['${value as string}'] is not valid`; + } + }, + }, + { + name: 'defaultOnRegister', + type: 'string', + nullable: true, + defaultValue: 'the-default', + }, + ], + } as CustomFields, +}); + +const registerDocument = graphqlShop(` + mutation RegisterWithCustomFields($emailAddress: String!, $value: String) { + registerCustomerAccount( + input: { emailAddress: $emailAddress, customFields: { validateOnRegister: $value } } + ) { + ... on Success { + success + } + ... on ErrorResult { + errorCode + message + } + } + } +`); + +const registerWithNullDefaultDocument = graphqlShop(` + mutation RegisterWithNullDefault($emailAddress: String!) { + registerCustomerAccount( + input: { emailAddress: $emailAddress, customFields: { defaultOnRegister: null } } + ) { + ... on Success { + success + } + ... on ErrorResult { + errorCode + message + } + } + } +`); + +const getCustomerByEmailDocument = graphql(` + query GetCustomerByEmail($emailAddress: String!) { + customers(options: { filter: { emailAddress: { eq: $emailAddress } } }) { + totalItems + items { + id + emailAddress + customFields { + validateOnRegister + defaultOnRegister + } + } + } + } +`); + +describe('Custom fields on RegisterCustomerInput', () => { + const { server, adminClient, shopClient } = createTestEnvironment(customConfig); + + beforeAll(async () => { + await server.init({ + initialData, + productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), + customerCount: 1, + }); + await adminClient.asSuperAdmin(); + }, TEST_SETUP_TIMEOUT_MS); + + afterAll(async () => { + await server.destroy(); + }); + + it('rejects an invalid value and creates no account', async () => { + await expect( + shopClient.query(registerDocument, { + emailAddress: 'register-validation-invalid@test.com', + value: 'nope', + }), + ).rejects.toThrow("The value ['nope'] is not valid"); + + const { customers } = await adminClient.query(getCustomerByEmailDocument, { + emailAddress: 'register-validation-invalid@test.com', + }); + + expect(customers.totalItems).toBe(0); + }); + + it('persists the custom field when the value is valid', async () => { + const { registerCustomerAccount } = await shopClient.query(registerDocument, { + emailAddress: 'register-validation-valid@test.com', + value: 'valid', + }); + + expect(registerCustomerAccount).toEqual({ success: true }); + + const { customers } = await adminClient.query(getCustomerByEmailDocument, { + emailAddress: 'register-validation-valid@test.com', + }); + + expect(customers.totalItems).toBe(1); + expect(customers.items[0].customFields.validateOnRegister).toBe('valid'); + }); + + it('applies default values to custom fields explicitly set to null', async () => { + const { registerCustomerAccount } = await shopClient.query(registerWithNullDefaultDocument, { + emailAddress: 'register-default@test.com', + }); + + expect(registerCustomerAccount).toEqual({ success: true }); + + const { customers } = await adminClient.query(getCustomerByEmailDocument, { + emailAddress: 'register-default@test.com', + }); + + expect(customers.totalItems).toBe(1); + expect(customers.items[0].customFields.defaultOnRegister).toBe('the-default'); + }); +}); diff --git a/packages/core/e2e/custom-fields.e2e-spec.ts b/packages/core/e2e/custom-fields.e2e-spec.ts index bddace0ee2d..c2b77858ca4 100644 --- a/packages/core/e2e/custom-fields.e2e-spec.ts +++ b/packages/core/e2e/custom-fields.e2e-spec.ts @@ -36,6 +36,7 @@ const customConfig = mergeConfig(testConfig(), { { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' }, { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' }, { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' }, + { name: 'localeTextField', type: 'localeText' }, { name: 'intWithDefault', type: 'int', defaultValue: 5 }, { name: 'floatWithDefault', type: 'float', defaultValue: 5.5678 }, { name: 'booleanWithDefault', type: 'boolean', defaultValue: true }, @@ -275,6 +276,7 @@ describe('Custom fields', () => { { name: 'notNullable', type: 'string', list: false }, { name: 'stringWithDefault', type: 'string', list: false }, { name: 'localeStringWithDefault', type: 'localeString', list: false }, + { name: 'localeTextField', type: 'localeText', list: false }, { name: 'intWithDefault', type: 'int', list: false }, { name: 'floatWithDefault', type: 'float', list: false }, { name: 'booleanWithDefault', type: 'boolean', list: false }, @@ -342,6 +344,7 @@ describe('Custom fields', () => { { name: 'notNullable', type: 'string', list: false }, { name: 'stringWithDefault', type: 'string', list: false }, { name: 'localeStringWithDefault', type: 'localeString', list: false }, + { name: 'localeTextField', type: 'localeText', list: false }, { name: 'intWithDefault', type: 'int', list: false }, { name: 'floatWithDefault', type: 'float', list: false }, { name: 'booleanWithDefault', type: 'boolean', list: false }, @@ -956,6 +959,13 @@ describe('Custom fields', () => { expect(products.totalItems).toBe(1); }); + // https://github.com/vendurehq/vendure/issues/5199 + it('can sort by localeText custom fields', async () => { + const { products } = await adminClient.query(getProductsSortByLocaleTextDocument); + + expect(products.totalItems).toBe(1); + }); + it('can filter by custom fields', async () => { const { products } = await adminClient.query(getProductsFilterByStringDocument); @@ -1533,6 +1543,14 @@ const getProductsSortByLocaleStringDocument = graphql(` } `); +const getProductsSortByLocaleTextDocument = graphql(` + query GetProductsSortByLocaleText { + products(options: { sort: { localeTextField: ASC } }) { + totalItems + } + } +`); + const getProductsFilterByStringDocument = graphql(` query GetProductsFilterByString { products(options: { filter: { stringWithDefault: { contains: "hello" } } }) { diff --git a/packages/core/e2e/default-scheduler-plugin.e2e-spec.ts b/packages/core/e2e/default-scheduler-plugin.e2e-spec.ts index 0be86e9951e..58c4a4b84fa 100644 --- a/packages/core/e2e/default-scheduler-plugin.e2e-spec.ts +++ b/packages/core/e2e/default-scheduler-plugin.e2e-spec.ts @@ -1,5 +1,5 @@ import { ConfigService, DefaultSchedulerPlugin, mergeConfig, ScheduledTask } from '@vendure/core'; -import { createTestEnvironment } from '@vendure/testing'; +import { createTestEnvironment, TestingLogger } from '@vendure/testing'; import path from 'path'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; @@ -15,8 +15,11 @@ const MAX_HOLD_MS = 5_000; const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +class EmptyMessageError extends Error {} + describe('Default scheduler plugin', () => { const taskSpy = vi.fn(); + const testingLogger = new TestingLogger(() => vi.fn()); // One task per hold-window test so DB state can't leak between them. const holdSpyBlocking = vi.fn(); const holdSpyManual = vi.fn(); @@ -34,6 +37,16 @@ describe('Default scheduler plugin', () => { return { success: true }; }, }), + new ScheduledTask({ + id: 'error-test-job', + description: 'A test job which throws an error with an empty message', + schedule: cron => cron.everySaturdayAt(0, 0), + async execute(injector) { + const error = new EmptyMessageError(''); + (error as Error & { cause?: unknown }).cause = 'ECONNRESET'; + throw error; + }, + }), new ScheduledTask({ id: 'hold-test-job-blocking', description: 'For testing the hold window blocks scheduled re-execution', @@ -56,6 +69,7 @@ describe('Default scheduler plugin', () => { runTasksInWorkerOnly: false, }, plugins: [DefaultSchedulerPlugin.init({ manualTriggerCheckInterval: 50 })], + logger: testingLogger, }), ); @@ -78,7 +92,7 @@ describe('Default scheduler plugin', () => { it('get tasks', async () => { const { scheduledTasks } = await adminClient.query(getTasksDocument); - expect(scheduledTasks.length).toBe(3); + expect(scheduledTasks.length).toBe(4); const testJob = scheduledTasks.find(t => t.id === 'test-job'); if (!testJob) throw new Error('test-job not found'); expect(testJob.description).toBe("A test job that doesn't do anything"); @@ -158,6 +172,29 @@ describe('Default scheduler plugin', () => { await strategy.executeTask(task)(); expect(holdSpyManual).toHaveBeenCalledTimes(2); }); + + // #5276 + it('logs the error class and cause when a task throws an empty-message error', async () => { + testingLogger.errorSpy.mockClear(); + + const { strategy, task } = getHoldTask(server, 'error-test-job'); + await strategy.executeTask(task)(); + + const call = testingLogger.errorSpy.mock.calls.find((args: any[]) => + String(args[0]).includes('Scheduled task "error-test-job" failed'), + ); + expect(call).toBeDefined(); + + const [message, , trace] = call as [string, string | undefined, string | undefined]; + expect(message).toContain('EmptyMessageError'); + expect(message).toContain('ECONNRESET'); + expect(message).toContain('at '); + expect(trace).toBeUndefined(); + + const { scheduledTasks } = await adminClient.query(getTasksDocument); + const errorTask = scheduledTasks.find(t => t.id === 'error-test-job'); + expect(errorTask?.lastResult).toEqual({ error: 'EmptyMessageError' }); + }); }); function getHoldTask(server: any, id: string) { diff --git a/packages/core/e2e/default-search-plugin.e2e-spec.ts b/packages/core/e2e/default-search-plugin.e2e-spec.ts index 698474046a6..6ab4034a5bd 100644 --- a/packages/core/e2e/default-search-plugin.e2e-spec.ts +++ b/packages/core/e2e/default-search-plugin.e2e-spec.ts @@ -168,6 +168,24 @@ describe('Default search plugin', () => { expect(result.search.items.map(i => i.productVariantName)).toEqual(expected); } + /** + * Clients generated from the GraphQL schema commonly send `sort: {}` when the user has not + * selected a sort order. Since an empty object is truthy, this used to be treated as an + * explicit sort, which suppressed the relevance ordering that applies when searching by term. + */ + async function testRelevanceSortingWithEmptySortObject(testProducts: TestProducts) { + const result = await testProducts({ + term: 'slr', + groupByProduct: true, + sort: {}, + }); + + // "Slr Camera" matches on the product name whereas "Camera Lens" only matches in the + // description, so the higher-scoring "Slr Camera" must come first, even though it has the + // higher productVariantId, which is the fallback ordering used when no score is applied. + expect(result.search.items.map(i => i.productName)).toEqual(['Slr Camera', 'Camera Lens']); + } + async function testMatchSearchTerm(testProducts: TestProducts) { const result = await testProducts({ term: 'camera', @@ -685,6 +703,9 @@ describe('Default search plugin', () => { it('sort price without grouping', () => testSortingNoGrouping(testProductsShop, 'price')); + it('sorts by relevance when sort is an empty object', () => + testRelevanceSortingWithEmptySortObject(testProductsShop)); + it('omits results for disabled ProductVariants', async () => { await adminClient.query(updateProductVariantsDocument, { input: [{ id: 'T_3', enabled: false }], @@ -822,6 +843,9 @@ describe('Default search plugin', () => { it('sort price without grouping', () => testSortingNoGrouping(testProductsAdmin, 'price')); + it('sorts by relevance when sort is an empty object', () => + testRelevanceSortingWithEmptySortObject(testProductsAdmin)); + describe('updating the index', () => { it('updates index when ProductVariants are changed', async () => { await awaitRunningJobs(adminClient); diff --git a/packages/core/e2e/order-promotion.e2e-spec.ts b/packages/core/e2e/order-promotion.e2e-spec.ts index a127ba04832..4c4dfffc792 100644 --- a/packages/core/e2e/order-promotion.e2e-spec.ts +++ b/packages/core/e2e/order-promotion.e2e-spec.ts @@ -2218,6 +2218,93 @@ describe('Promotions applied to Orders', () => { }); }); + describe('promotion which applies no discount', () => { + const couponCode = 'NO_DISCOUNT_ON_THIS_CART'; + let promotion: PromotionFragment; + + beforeAll(async () => { + promotion = await createPromotion({ + enabled: true, + name: '50% off item-5000 only', + couponCode, + perCustomerUsageLimit: 1, + conditions: [], + actions: [ + { + code: productsPercentageDiscount.code, + arguments: [ + { name: 'discount', value: '50' }, + { + name: 'productVariantIds', + value: `["${getVariantBySlug('item-5000').id}"]`, + }, + ], + }, + ], + }); + }); + + afterAll(async () => { + await deletePromotion(promotion.id); + }); + + function addGuestCustomerToOrder() { + return shopClient.query(setCustomerDocument, { + input: { + emailAddress: 'no-discount-guest@test.com', + firstName: 'No Discount', + lastName: 'Customer', + }, + }); + } + + it('is not added to the Order and does not consume a usage', async () => { + await shopClient.asAnonymousUser(); + await shopClient.query(addItemToOrderDocument, { + // Not one of the Promotion's ProductVariants, so the action discounts nothing + productVariantId: getVariantBySlug('item-1000').id, + quantity: 1, + }); + const { setCustomerForOrder } = await addGuestCustomerToOrder(); + orderResultGuard.assertSuccess(setCustomerForOrder); + + const { applyCouponCode } = await shopClient.query(applyCouponCodeDocument, { couponCode }); + orderResultGuard.assertSuccess(applyCouponCode); + + // The customer typed the code, so it stays on the Order even though it discounts nothing + expect(applyCouponCode.couponCodes).toEqual([couponCode]); + expect(applyCouponCode.discounts).toEqual([]); + const orderCode = applyCouponCode.code; + + await proceedToArrangingPayment(shopClient); + const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod); + orderResultGuard.assertSuccess(order); + expect(order.state).toBe('PaymentSettled'); + + const { orderByCode } = await shopClient.query(getOrderPromotionsByCodeDocument, { + code: orderCode, + }); + expect(orderByCode!.promotions).toEqual([]); + + // The customer's single allowed usage is still available + await shopClient.asAnonymousUser(); + await shopClient.query(addItemToOrderDocument, { + productVariantId: getVariantBySlug('item-5000').id, + quantity: 1, + }); + const { setCustomerForOrder: setCustomerForSecondOrder } = await addGuestCustomerToOrder(); + orderResultGuard.assertSuccess(setCustomerForSecondOrder); + + const { applyCouponCode: secondUse } = await shopClient.query(applyCouponCodeDocument, { + couponCode, + }); + orderResultGuard.assertSuccess(secondUse); + + expect(secondUse.discounts.length).toBe(1); + expect(secondUse.totalWithTax).toBe(3000); + }); + }); + // https://github.com/vendurehq/vendure/issues/710 it('removes order-level discount made invalid by removing OrderLine', async () => { const promotion = await createPromotion({ diff --git a/packages/core/e2e/order.e2e-spec.ts b/packages/core/e2e/order.e2e-spec.ts index 52710627b7b..3b897d84307 100644 --- a/packages/core/e2e/order.e2e-spec.ts +++ b/packages/core/e2e/order.e2e-spec.ts @@ -12,8 +12,10 @@ import { import { omit } from '@vendure/common/lib/omit'; import { pick } from '@vendure/common/lib/pick'; import { + Order, OrderService, RequestContextService, + TransactionalConnection, defaultShippingCalculator, defaultShippingEligibilityChecker, manualFulfillmentHandler, @@ -2453,6 +2455,56 @@ describe('Orders resolver', () => { shopOrderGuard.assertSuccess(secondAdd); expect(secondAdd.lines[0].quantity).toBe(2); }); + + // https://github.com/vendurehq/vendure/pull/5164 + // Every Order mutation holds `order.surcharges` as a snapshot taken when the Order + // was loaded. Saving the Order must not act on that snapshot: a Surcharge added + // since the load, by a blocking event handler or by a concurrent request, would + // otherwise have its `orderId` set to null. + it('does not detach Surcharges when saving an Order with a stale surcharges array', async () => { + await shopClient.asAnonymousUser(); + const { addItemToOrder: add } = await shopClient.query(addItemToOrderDocument, { + productVariantId: 'T_1', + quantity: 1, + }); + shopOrderGuard.assertSuccess(add); + const internalOrderId = +add.id.replace('T_', ''); + + const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' }); + const orderService = server.app.get(OrderService); + const connection = server.app.get(TransactionalConnection); + + await orderService.addSurchargeToOrder(ctx, internalOrderId, { + description: 'Gift wrap', + listPrice: 200, + }); + + const staleOrder = await connection.getEntityOrThrow(ctx, Order, internalOrderId, { + // `lines` is joined because saving an Order reads its calculated `discounts` + // property, via the enumerable getter installed by the calculated-property + // subscriber, and that getter throws when the relation is missing. + relations: ['surcharges', 'lines'], + }); + expect(staleOrder.surcharges.length).toBe(1); + + // A blocking event handler or a concurrent request adds a second Surcharge, which + // the array held by `staleOrder` does not know about. + await orderService.addSurchargeToOrder(ctx, internalOrderId, { + description: 'Loyalty discount', + listPrice: -500, + }); + + await connection.getRepository(ctx, Order).save(staleOrder); + + const surcharges = await orderService.getOrderSurcharges(ctx, internalOrderId); + expect(surcharges.map(s => s.listPrice).sort((a, b) => a - b)).toEqual([-500, 200]); + + // Removal deletes the row, so it does not depend on the Order save detaching it. + const loyaltyDiscount = surcharges.find(s => s.description === 'Loyalty discount')!; + await orderService.removeSurchargeFromOrder(ctx, internalOrderId, loyaltyDiscount.id); + const remaining = await orderService.getOrderSurcharges(ctx, internalOrderId); + expect(remaining.map(s => s.description)).toEqual(['Gift wrap']); + }); }); describe('set order currency code', () => { diff --git a/packages/core/package.json b/packages/core/package.json index 79e3b708472..9003ccc2b2f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -41,7 +41,9 @@ ], "dependencies": { "@apollo/server": "^4.12.2", + "@apollographql/graphql-playground-html": "^1.6.29", "@graphql-tools/stitch": "^10.1.16", + "@graphql-tools/utils": "^11.0.0", "@nestjs/apollo": "~13.1.0", "@nestjs/common": "^11.0.12", "@nestjs/core": "^11.0.12", diff --git a/packages/core/src/api/middleware/custom-field-processing-interceptor.ts b/packages/core/src/api/middleware/custom-field-processing-interceptor.ts index fc336a53c8e..14055875e01 100644 --- a/packages/core/src/api/middleware/custom-field-processing-interceptor.ts +++ b/packages/core/src/api/middleware/custom-field-processing-interceptor.ts @@ -43,6 +43,10 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { }); // Note: OrderLineCustomFieldsInput is handled separately since it's used in both // create operations (addItemToOrder) and update operations (adjustOrderLine) + + // RegisterCustomerInput carries Customer custom fields but is not named CreateCustomerInput. + // getEntityNameFromInputType() maps it back to Customer. + this.createInputsWithCustomFields.add('RegisterCustomerInput'); } async intercept(context: ExecutionContext, next: CallHandler) { @@ -217,6 +221,10 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { } private getEntityNameFromInputType(typeName: string): string { + if (typeName === 'RegisterCustomerInput') { + // Added to createInputsWithCustomFields in the constructor. + return 'Customer'; + } // Remove "Create" or "Update" prefix and "Input" suffix // e.g., "CreateProductInput" -> "Product", "UpdateCustomerInput" -> "Customer" if (typeName.startsWith('Create')) { @@ -235,7 +243,7 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { variableValues?: { [key: string]: any }, ) { if (variableValues) { - const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2'); + const entityName = this.getEntityNameFromInputType(typeName); const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields]; if (typeName === 'OrderLineCustomFieldsInput') { diff --git a/packages/core/src/config/promotion/promotion-action.ts b/packages/core/src/config/promotion/promotion-action.ts index a27ef4e8208..d24d9cf3d96 100644 --- a/packages/core/src/config/promotion/promotion-action.ts +++ b/packages/core/src/config/promotion/promotion-action.ts @@ -306,6 +306,17 @@ export abstract class PromotionAction< this.onDeactivateFn = config.onDeactivate; } + /** + * @description + * Whether this action defines any of the side-effect functions. Such an action can have an + * effect on the Order (adding a free gift, say) without producing a price adjustment. + * + * @internal + */ + get hasSideEffects(): boolean { + return !!(this.onActivateFn || this.onDeactivateFn); + } + /** @internal */ abstract execute(...arg: any[]): number | Promise; diff --git a/packages/core/src/config/system/health-check-strategy.ts b/packages/core/src/config/system/health-check-strategy.ts index 55ab552af33..e197fd802f3 100644 --- a/packages/core/src/config/system/health-check-strategy.ts +++ b/packages/core/src/config/system/health-check-strategy.ts @@ -1,26 +1,13 @@ -import { HealthIndicatorFunction } from '../../health-check/terminus-compat'; import { InjectableStrategy } from '../../common/types/injectable-strategy'; +import { HealthIndicatorFunction } from '../../health-check/terminus-compat'; /** * @description - * This strategy defines health checks which are included as part of the - * `/health` endpoint. They should only be used to monitor _critical_ systems - * on which proper functioning of the Vendure server depends. - * - * Custom strategies should be added to the `systemOptions.healthChecks` array. - * By default, Vendure includes the `TypeORMHealthCheckStrategy`, so if you set the value of the `healthChecks` - * array, be sure to include it manually. - * - * Vendure also ships with the {@link HttpHealthCheckStrategy}, which is convenient - * for adding a health check dependent on an HTTP ping. - * - * :::info - * - * This is configured via the `systemOptions.healthChecks` property of - * your VendureConfig. - * - * ::: + * This strategy defines health checks which, before v3.6.0, were run by the `/health` endpoint. + * Since v3.6.0 the strategies configured in `systemOptions.healthChecks` are not executed, and this + * interface will be removed in v4.0.0. See the [health checks guide](/core-concepts/healthchecks/). * + * The example below shows how strategies were configured before v3.6.0. * * @example * ```ts @@ -40,14 +27,14 @@ import { InjectableStrategy } from '../../common/types/injectable-strategy'; * ``` * * @docsCategory health-check - * @deprecated Use infrastructure-level health checks (e.g. Kubernetes probes, Docker healthchecks, + * @deprecated Not executed since v3.6.0. Use infrastructure-level health checks (e.g. Kubernetes probes, Docker healthchecks, * load balancer checks) instead of application-level health checks. This interface will be removed in v4.0.0. */ export interface HealthCheckStrategy extends InjectableStrategy { /** * @description * Should return a {@link HealthIndicatorFunction} which performs the check - * and resolves to a status payload. + * and resolves to a status payload. Since v3.6.0 this method is never called. */ getHealthIndicator(): HealthIndicatorFunction; } diff --git a/packages/core/src/config/vendure-config.ts b/packages/core/src/config/vendure-config.ts index d38614975bd..fa8d82ea4c3 100644 --- a/packages/core/src/config/vendure-config.ts +++ b/packages/core/src/config/vendure-config.ts @@ -1281,12 +1281,13 @@ export interface EntityOptions { export interface SystemOptions { /** * @description - * Defines an array of {@link HealthCheckStrategy} instances which are used by the `/health` endpoint to verify - * that any critical systems which the Vendure server depends on are also healthy. + * Defines an array of {@link HealthCheckStrategy} instances. Before v3.6.0 the `/health` endpoint ran these + * strategies to verify that critical systems which the Vendure server depends on were healthy. Since v3.6.0 + * the strategies are not executed and this option has no effect on the `/health` response. * - * @default [TypeORMHealthCheckStrategy] + * @default [] * @since 1.6.0 - * @deprecated Use infrastructure-level health checks (e.g. Kubernetes probes, Docker healthchecks, + * @deprecated Not executed since v3.6.0. Use infrastructure-level health checks (e.g. Kubernetes probes, Docker healthchecks, * load balancer checks) instead of application-level health checks. The application should not * be responsible for determining its own health. This config option will be removed in v4.0.0. */ diff --git a/packages/core/src/entity/index.ts b/packages/core/src/entity/index.ts index 3ae9e3cf590..f8158acc326 100644 --- a/packages/core/src/entity/index.ts +++ b/packages/core/src/entity/index.ts @@ -1,5 +1,6 @@ export * from './address/address.entity'; export * from './administrator/administrator.entity'; +export * from './api-key/api-key.entity'; export * from './asset/asset-translation.entity'; export * from './asset/asset.entity'; export * from './asset/orderable-asset.entity'; @@ -21,12 +22,16 @@ export * from './facet/facet-translation.entity'; export * from './facet/facet.entity'; export * from './fulfillment/fulfillment.entity'; export * from './global-settings/global-settings.entity'; +export * from './history-entry/customer-history-entry.entity'; +export * from './history-entry/history-entry.entity'; +export * from './history-entry/order-history-entry.entity'; export * from './money.decorator'; export * from './order-line-reference/fulfillment-line.entity'; export * from './order-line-reference/order-line-reference.entity'; export * from './order-line-reference/order-modification-line.entity'; export * from './order-line-reference/refund-line.entity'; export * from './order-line/order-line.entity'; +export * from './order-modification/order-modification.entity'; export * from './order/order.entity'; export * from './payment-method/payment-method.entity'; export * from './payment/payment.entity'; @@ -45,6 +50,7 @@ export * from './promotion/promotion.entity'; export * from './refund/refund.entity'; export * from './region/country.entity'; export * from './region/province.entity'; +export * from './region/region.entity'; export * from './role/role.entity'; export * from './seller/seller.entity'; export * from './session/anonymous-session.entity'; diff --git a/packages/core/src/entity/order/order.entity.ts b/packages/core/src/entity/order/order.entity.ts index 634940417ae..87e6351ae48 100644 --- a/packages/core/src/entity/order/order.entity.ts +++ b/packages/core/src/entity/order/order.entity.ts @@ -107,6 +107,11 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField * ProductVariants nor discounts resulting from applied Promotions. For example, * one-off discounts based on customer interaction, or surcharges based on payment * methods. + * + * Surcharges are attached and removed with {@link OrderService.addSurchargeToOrder} and + * {@link OrderService.removeSurchargeFromOrder}. Saving an Order does not detach a + * Surcharge which is missing from this array, and repricing the Order does not attach one + * which was saved without its `order` property set. */ @OneToMany(type => Surcharge, surcharge => surcharge.order) surcharges: Surcharge[]; diff --git a/packages/core/src/entity/promotion/promotion.entity.ts b/packages/core/src/entity/promotion/promotion.entity.ts index 1cdad67147b..7a9ad90ac10 100644 --- a/packages/core/src/entity/promotion/promotion.entity.ts +++ b/packages/core/src/entity/promotion/promotion.entity.ts @@ -147,6 +147,18 @@ export class Promotion */ @Column() priorityScore: number; + /** + * @description + * Whether any of this Promotion's actions define a side effect (`onActivate` / `onDeactivate`). + * Such a Promotion can benefit the Order without producing a price adjustment - the free gift + * action being the canonical example - so it counts as applied even when it discounts nothing. + * + * @internal + */ + get hasSideEffects(): boolean { + return this.actions.some(action => this.allActions[action.code]?.hasSideEffects); + } + async apply( ctx: RequestContext, args: ApplyOrderActionArgs | ApplyOrderItemActionArgs | ApplyShippingActionArgs, diff --git a/packages/core/src/entity/surcharge/surcharge.entity.ts b/packages/core/src/entity/surcharge/surcharge.entity.ts index 432ebb4d00a..acdd2b25e46 100644 --- a/packages/core/src/entity/surcharge/surcharge.entity.ts +++ b/packages/core/src/entity/surcharge/surcharge.entity.ts @@ -39,8 +39,16 @@ export class Surcharge extends VendureEntity { @Column('simple-json') taxLines: TaxLine[]; + // TypeORM's default 'nullify' sets `orderId` to null on every Surcharge row missing from + // the in-memory `Order.surcharges` array, which is a snapshot from the time the Order was + // loaded: a Surcharge added since, by an event handler or by a concurrent request, is + // detached by the next save of that Order. `OrderService.removeSurchargeFromOrder` deletes + // the row, so nothing needs the nullify. @Index() - @ManyToOne(type => Order, order => order.surcharges, { onDelete: 'CASCADE' }) + @ManyToOne(type => Order, order => order.surcharges, { + onDelete: 'CASCADE', + orphanedRowAction: 'disable', + }) order: Order; @Index() diff --git a/packages/core/src/health-check/health-check-registry.service.ts b/packages/core/src/health-check/health-check-registry.service.ts index c714696aa31..c37f90a529d 100644 --- a/packages/core/src/health-check/health-check-registry.service.ts +++ b/packages/core/src/health-check/health-check-registry.service.ts @@ -2,22 +2,11 @@ import { HealthIndicatorFunction } from './terminus-compat'; /** * @description - * This service is used to register health indicator functions to be included in the - * health check. Health checks can be used by automated services such as Kubernetes - * to determine the state of applications it is running. They are also useful for - * administrators to get an overview of the health of all the parts of the - * Vendure stack. + * This service is used to register health indicator functions which, before v3.6.0, were run by the + * `/health` endpoint. Since v3.6.0 registered indicators are not executed, and this service will be + * removed in v4.0.0. See the [health checks guide](/core-concepts/healthchecks/). * - * Plugins which rely on external services (web services, databases etc.) can make use of this - * service to add a check for that dependency to the Vendure health check. - * - * - * Since v1.6.0, the preferred way to implement a custom health check is by creating a new {@link HealthCheckStrategy} - * and then passing it to the `systemOptions.healthChecks` array. - * See the {@link HealthCheckStrategy} docs for an example configuration. - * - * The alternative way to register a health check is by injecting this service directly into your - * plugin module. To use it in your plugin, you'll need to import the {@link PluginCommonModule}: + * The example below shows how a plugin registered an indicator before v3.6.0: * * @example * ```ts @@ -48,9 +37,7 @@ export class HealthCheckRegistryService { /** * @description - * Registers one or more {@link HealthIndicatorFunction}s to be added to the - * health check endpoint. The indicator will also appear in the Admin UI's - * "system status" view. + * Registers one or more {@link HealthIndicatorFunction}s. Since v3.6.0 the registered functions are never called. * * @deprecated Use infrastructure-level health checks instead. This method will be removed in v4.0.0. */ diff --git a/packages/core/src/health-check/http-health-check-strategy.ts b/packages/core/src/health-check/http-health-check-strategy.ts index fcf26cc703d..0edb45e69cc 100644 --- a/packages/core/src/health-check/http-health-check-strategy.ts +++ b/packages/core/src/health-check/http-health-check-strategy.ts @@ -21,7 +21,8 @@ export interface HttpHealthCheckOptions { /** * @description - * A {@link HealthCheckStrategy} used to check health by pinging a url. + * A {@link HealthCheckStrategy} used to check health by pinging a url. Since v3.6.0 it is not + * executed and will be removed in v4.0.0. The example below shows the pre-v3.6.0 configuration: * * @example * ```ts diff --git a/packages/core/src/health-check/terminus-compat.ts b/packages/core/src/health-check/terminus-compat.ts index 6c39c60940e..ce46227e8b7 100644 --- a/packages/core/src/health-check/terminus-compat.ts +++ b/packages/core/src/health-check/terminus-compat.ts @@ -55,8 +55,8 @@ export type HealthIndicatorFunction = () => /** * @description - * Thrown from a health indicator to signal a failed check. The `causes` - * payload is forwarded to the `/health` response so callers can inspect + * Thrown from a health indicator to signal a failed check. Before v3.6.0 the `causes` + * payload was forwarded to the `/health` response so callers could inspect * which indicator failed and why. `causes` is intentionally typed as * `any` (matching terminus) so handlers can pass through arbitrary * upstream error payloads (HTTP responses, DB driver errors, etc.) diff --git a/packages/core/src/health-check/typeorm-health-check-strategy.ts b/packages/core/src/health-check/typeorm-health-check-strategy.ts index 46474c41158..3d814ebb01a 100644 --- a/packages/core/src/health-check/typeorm-health-check-strategy.ts +++ b/packages/core/src/health-check/typeorm-health-check-strategy.ts @@ -14,9 +14,8 @@ export interface TypeORMHealthCheckOptions { /** * @description - * A {@link HealthCheckStrategy} used to check the health of the database. This health - * check is included by default, but can be customized by explicitly adding it to the - * `systemOptions.healthChecks` array: + * A {@link HealthCheckStrategy} used to check the health of the database. Since v3.6.0 it is not + * executed and will be removed in v4.0.0. The example below shows the pre-v3.6.0 configuration: * * @example * ```ts @@ -27,8 +26,6 @@ export interface TypeORMHealthCheckOptions { * systemOptions: { * healthChecks:[ * // The default key is "database" and the default timeout is 1000ms - * // Sometimes this is too short and leads to false negatives in the - * // /health endpoint. * new TypeORMHealthCheckStrategy({ key: 'postgres-db', timeout: 5000 }), * ] * } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7ba1b021055..d31e6bda952 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,6 +19,7 @@ export * from './health-check/index'; export * from './i18n/index'; export * from './job-queue/index'; export { generateMigration, revertLastMigration, runMigrations } from './migrate'; +export type { MigrationDiagnostic, RunMigrationsOptions } from './migrate'; export * from './migration-utils/index'; export * from './plugin/index'; export * from './process-context/index'; diff --git a/packages/core/src/job-queue/index.ts b/packages/core/src/job-queue/index.ts index 57618aa8017..024ffc04737 100644 --- a/packages/core/src/job-queue/index.ts +++ b/packages/core/src/job-queue/index.ts @@ -8,4 +8,5 @@ export * from './job-queue'; export * from './job-queue.service'; export * from './polling-job-queue-strategy'; export * from './queue-name-process-storage'; +export * from './subscribable-job'; export * from './types'; diff --git a/packages/core/src/migrate.ts b/packages/core/src/migrate.ts index 66fc0913a7c..6e2920d88d5 100644 --- a/packages/core/src/migrate.ts +++ b/packages/core/src/migrate.ts @@ -2,13 +2,14 @@ import fs from 'fs-extra'; import path from 'path'; import pc from 'picocolors'; -import { Connection, createConnection, DataSourceOptions } from 'typeorm'; +import { Connection, createConnection, DataSourceOptions, MigrationExecutor } from 'typeorm'; import { MysqlDriver } from 'typeorm/driver/mysql/MysqlDriver'; import { camelCase } from 'typeorm/util/StringUtils'; import { preBootstrapConfig } from './bootstrap'; import { resetConfig } from './config/config-helpers'; import { VendureConfig } from './config/vendure-config'; +import { Product } from './entity/product/product.entity'; /** * @description @@ -30,6 +31,51 @@ export interface MigrationOptions { outputDir?: string; } +/** + * @description + * A condition detected while running migrations which cannot be inferred from the return value + * of {@link runMigrations}. + * + * @docsCategory migration + * @since 3.7.4 + */ +export interface MigrationDiagnostic { + /** + * @description + * Identifies the condition: + * + * - `no-migrations-matched`: the configured `migrations` patterns matched no files, but the + * database has migrations recorded as applied. The patterns must therefore have stopped + * matching, for example because they point at compiled output which has not been built. + * - `schema-out-of-sync`: the database schema does not match the current entity + * configuration, so a new migration needs to be generated. + */ + code: 'no-migrations-matched' | 'schema-out-of-sync'; + /** + * @description + * The condition rendered as lines of human-readable text. + */ + lines: string[]; +} + +/** + * @description + * Options for {@link runMigrations}. + * + * @docsCategory migration + * @since 3.7.4 + */ +export interface RunMigrationsOptions { + /** + * @description + * Invoked for each {@link MigrationDiagnostic} detected during the run. These conditions are + * also printed to the console, unless the `VENDURE_RUNNING_IN_CLI` environment variable is + * set, which the Vendure CLI does so that it can render them itself. Passing this callback + * does not suppress that output. + */ + onDiagnostic?: (diagnostic: MigrationDiagnostic) => void; +} + /** * @description * Runs any pending database migrations. See [TypeORM migration docs](https://typeorm.io/#/migrations) @@ -37,11 +83,22 @@ export interface MigrationOptions { * * @docsCategory migration */ -export async function runMigrations(userConfig: Partial): Promise { +export async function runMigrations( + userConfig: Partial, + options?: RunMigrationsOptions, +): Promise { const config = await preBootstrapConfig(userConfig); const connection = await createConnection(createConnectionOptions(config)); const migrationsRan: string[] = []; + const report = (diagnostic: MigrationDiagnostic) => { + options?.onDiagnostic?.(diagnostic); + log(pc.yellow(diagnostic.lines.join('\n'))); + }; try { + const unmatched = await detectUnmatchedPatterns(connection); + if (unmatched) { + report(unmatched); + } const migrations = await disableForeignKeysForSqLite(connection, () => connection.runMigrations({ transaction: 'each' }), ); @@ -58,27 +115,105 @@ export async function runMigrations(userConfig: Partial): Promise process.exitCode = 1; } } finally { - await checkMigrationStatus(connection); - await connection.close(); - resetConfig(); + try { + await checkMigrationStatus(connection, report); + } finally { + await connection.close(); + resetConfig(); + } } return migrationsRan; } -async function checkMigrationStatus(connection: Connection) { +async function checkMigrationStatus(connection: Connection, report: (d: MigrationDiagnostic) => void) { + // Against a database with no tables at all the schema builder reports the entire schema as + // pending, which is what a project looks like before it has been set up. That is not drift, + // so there is nothing useful to say about it. + // + // The `migrations` table is the wrong discriminator here: a schema built with + // `synchronize: true` has every table and no migration history, because TypeORM only writes + // that table when it runs a migration. Reporting drift is exactly what that developer needs + // when they switch to `synchronize: false` and change an entity. + if (!(await hasBeenSetUp(connection))) { + return; + } const builderLog = await connection.driver.createSchemaBuilder().log(); if (builderLog.upQueries.length) { - log( - pc.yellow( + report({ + code: 'schema-out-of-sync', + lines: [ 'Your database schema does not match your current configuration. Generate a new migration for the following changes:', - ), - ); - for (const query of builderLog.upQueries) { - log(' - ' + pc.yellow(query.query)); - } + ...builderLog.upQueries.map(q => ' - ' + q.query), + ], + }); + } +} + +/** + * Asking about one known table keeps this to a single statement. Loading every entity table + * would answer the same question, but `createSchemaBuilder().log()` already does that on its + * own query runner, so the work would be done twice on every run. + */ +async function hasBeenSetUp(connection: Connection): Promise { + const queryRunner = connection.createQueryRunner(); + try { + return await queryRunner.hasTable(connection.getMetadata(Product).tablePath); + } finally { + await queryRunner.release(); } } +/** + * TypeORM resolves migration globs relative to `process.cwd()` and silently yields zero classes + * when nothing matches. That is indistinguishable from "every migration has already been + * applied" in the return value of `runMigrations()`, so the command reports success while + * leaving the database untouched. + * + * Zero loaded classes on its own is not evidence of a problem: it is also what a project looks + * like before its first migration is authored, which is how the `create` scaffold ships. The + * discriminator is the `migrations` table. If the database has migrations on record but nothing + * loaded, the patterns can only have stopped matching. + */ +async function detectUnmatchedPatterns(connection: Connection): Promise { + // Giving up as soon as any class loaded means a config which mixes a working pattern with a + // broken one will not warn. That is no worse than the current behaviour. + if (connection.migrations.length) { + return; + } + const patterns = getConfiguredPatterns(connection.options.migrations); + if (!patterns.length) { + return; + } + // Creates the `migrations` table as a side effect, which `runMigrations()` does anyway on + // the next line. Returns an empty array rather than throwing against a fresh database. + const executed = await new MigrationExecutor(connection).getExecutedMigrations(); + if (!executed.length) { + return; + } + // The cwd only explains the failure for a relative pattern. The scaffolded config uses + // `path.join(__dirname, ...)`, where pointing at the working directory sends the user looking + // for a problem that is not there. + const anyRelative = patterns.some(pattern => !path.isAbsolute(pattern)); + return { + code: 'no-migrations-matched', + lines: [ + 'No migration files matched the configured `migrations` patterns, but this database has migrations recorded as applied.', + 'Nothing on disk matches these patterns. If they point at compiled output, check that it has been built:', + ...patterns.map(pattern => ' - ' + pattern), + ...(anyRelative + ? [`Relative patterns are resolved against the current directory (${process.cwd()}).`] + : []), + ], + }; +} + +function getConfiguredPatterns(configuredMigrations: DataSourceOptions['migrations']): string[] { + const entries = Array.isArray(configuredMigrations) + ? configuredMigrations + : Object.values(configuredMigrations ?? {}); + return entries.filter((entry): entry is string => typeof entry === 'string'); +} + /** * @description * Reverts the last applied database migration. See [TypeORM migration docs](https://typeorm.io/#/migrations) diff --git a/packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts b/packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts index 6b457faf06b..73b2c2de15f 100644 --- a/packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts +++ b/packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts @@ -1,6 +1,7 @@ import { UpdateScheduledTaskInput } from '@vendure/common/lib/generated-types'; import { Cron } from 'croner'; import ms, { type StringValue } from 'ms'; +import { inspect } from 'node:util'; import { Injector } from '../../common'; import { assertFound } from '../../common/utils'; @@ -122,17 +123,13 @@ export class DefaultSchedulerStrategy implements SchedulerStrategy { ); Logger.verbose(`Scheduled task "${task.id}" completed successfully`); } catch (error) { - let errorMessage = 'Unknown error'; - if (error instanceof Error) { - errorMessage = error.message; - } - Logger.error(`Scheduled task "${task.id}" failed with error: ${errorMessage}`); + Logger.error(`Scheduled task "${task.id}" failed with error: ${inspect(error)}`); await this.connection.rawConnection.getRepository(ScheduledTaskRecord).update( { taskId: task.id }, { lastExecutedAt: new Date(), lockedAt: null, - lastResult: { error: errorMessage } as any, + lastResult: { error: errorLabel(error) } as any, }, ); } finally { @@ -357,3 +354,14 @@ export class DefaultSchedulerStrategy implements SchedulerStrategy { } } } + +/** + * `constructor.name` rather than `name` because a subclass which does not set `name` + * still inherits `'Error'` from `Error.prototype`. + */ +function errorLabel(error: unknown): string { + if (!(error instanceof Error)) { + return 'Unknown error'; + } + return error.message || error.constructor.name; +} diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts index 43f55524aa3..b19f85172a8 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts @@ -17,6 +17,7 @@ import { createCollectionIdCountMap, createFacetIdCountMap, createPlaceholderFromId, + hasSortParameter, mapToSearchResult, } from './search-strategy-utils'; @@ -100,7 +101,7 @@ export class MysqlSearchStrategy implements SearchStrategy { this.applyTermAndFilters(ctx, qb, input); - if (sort) { + if (hasSortParameter(sort)) { if (sort.name) { qb.addOrderBy('si_productName', sort.name); } diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts index a1ddcbb525c..064279512b7 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts @@ -17,6 +17,7 @@ import { createCollectionIdCountMap, createFacetIdCountMap, createPlaceholderFromId, + hasSortParameter, mapToSearchResult, } from './search-strategy-utils'; @@ -100,7 +101,7 @@ export class PostgresSearchStrategy implements SearchStrategy { this.applyTermAndFilters(ctx, qb, input); - if (sort) { + if (hasSortParameter(sort)) { if (sort.name) { qb.addOrderBy('"si_productName"', sort.name); } diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.spec.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.spec.ts new file mode 100644 index 00000000000..9e5081d420c --- /dev/null +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.spec.ts @@ -0,0 +1,31 @@ +import { SearchResultSortParameter, SortOrder } from '@vendure/common/lib/generated-types'; +import { describe, expect, it } from 'vitest'; + +import { hasSortParameter } from './search-strategy-utils'; + +describe('hasSortParameter()', () => { + it('returns false when no sort parameter was given', () => { + expect(hasSortParameter(undefined)).toBe(false); + expect(hasSortParameter(null)).toBe(false); + }); + + it('returns false for an empty sort object', () => { + expect(hasSortParameter({})).toBe(false); + }); + + it('returns false when all sort fields are null or undefined', () => { + // GraphQL can deliver an explicit null at runtime, which the generated input type does not model + expect( + hasSortParameter({ name: null, price: undefined } as unknown as SearchResultSortParameter), + ).toBe(false); + }); + + it('returns true when at least one sort field is set', () => { + expect(hasSortParameter({ name: SortOrder.ASC })).toBe(true); + expect(hasSortParameter({ price: SortOrder.DESC })).toBe(true); + // GraphQL can deliver an explicit null at runtime, which the generated input type does not model + expect( + hasSortParameter({ name: null, price: SortOrder.ASC } as unknown as SearchResultSortParameter), + ).toBe(true); + }); +}); diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts index 0a551f9508b..ea7ea7095d8 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts @@ -5,6 +5,7 @@ import { PriceRange, SearchResult, SearchResultAsset, + SearchResultSortParameter, SinglePrice, } from '@vendure/common/lib/generated-types'; import { ID } from '@vendure/common/lib/shared-types'; @@ -116,6 +117,18 @@ export function createPlaceholderFromId(id: ID): string { return '_' + id.toString().replace(/-/g, '_'); } +/** + * Returns true if the given sort parameter actually specifies a field to sort by. + * + * Clients built with GraphQL code generation commonly send `sort: {}` when the user has + * not selected any sort order. Since an empty object is truthy, it must not be treated as + * an explicit sort, otherwise no ordering would be applied at all and the relevance + * ordering which applies when searching by term would be silently suppressed. + */ +export function hasSortParameter(sort?: SearchResultSortParameter | null): sort is SearchResultSortParameter { + return sort != null && Object.values(sort).some(value => value != null); +} + /** * Applies language constraints for {@link SearchIndexItem} query. * diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts index 0188df2213e..7dd7070066b 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts @@ -16,6 +16,7 @@ import { createCollectionIdCountMap, createFacetIdCountMap, createPlaceholderFromId, + hasSortParameter, mapToSearchResult, } from './search-strategy-utils'; @@ -99,7 +100,7 @@ export class SqliteSearchStrategy implements SearchStrategy { this.applyTermAndFilters(ctx, qb, input); - if (sort) { + if (hasSortParameter(sort)) { if (sort.name) { // TODO: v3 - set the collation on the SearchIndexItem entity qb.addOrderBy('si.productName COLLATE NOCASE', sort.name); diff --git a/packages/core/src/plugin/plugin-utils.ts b/packages/core/src/plugin/plugin-utils.ts index 12016f800c8..415c5690829 100644 --- a/packages/core/src/plugin/plugin-utils.ts +++ b/packages/core/src/plugin/plugin-utils.ts @@ -44,12 +44,6 @@ export function createProxyHandler(options: ProxyOptions): RequestHandler { [`^${route}`]: '/' + (options.basePath || ''), }, logger: { - log(message: string) { - Logger.debug(message, options.label); - }, - debug(message: string) { - Logger.debug(message, options.label); - }, info(message: string) { Logger.debug(message, options.label); }, diff --git a/packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts b/packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts index 07417fcb0aa..31368a00351 100644 --- a/packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts +++ b/packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts @@ -116,6 +116,31 @@ describe('parseSortParams()', () => { }); }); + // #5199 — a localeText custom field must resolve to the same customFields path as localeString + it('works with localeText custom fields', () => { + const connection = new MockConnection(); + connection.setColumns(Product, [{ propertyName: 'id' }]); + connection.setRelations(Product, [{ propertyName: 'translations', type: ProductTranslation }]); + connection.setColumns(ProductTranslation, [{ propertyName: 'id' }, { propertyName: 'summary' }]); + + const sortParams: SortParameter = { + summary: 'ASC', + }; + const productCustomFields: CustomFieldConfig[] = [{ name: 'summary', type: 'localeText' }]; + + const result = parseSortParams( + connection as any, + Product, + sortParams, + {}, + undefined, + productCustomFields, + ); + expect(result).toEqual({ + 'product__translations.customFields.summary': 'ASC', + }); + }); + it('throws if an invalid field is passed', () => { const connection = new MockConnection(); connection.setColumns(Product, [{ propertyName: 'id' }, { propertyName: 'image' }]); diff --git a/packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts b/packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts index eec3f522aac..3e1e09522fc 100644 --- a/packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts +++ b/packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts @@ -46,9 +46,10 @@ export function parseSortParams( const translationsAlias = connection.namingStrategy.joinTableName(alias, 'translations', '', ''); const pathParts = [translationsAlias]; - const isLocaleStringCustomField = - customFields?.find(f => f.name === key)?.type === 'localeString'; - if (isLocaleStringCustomField) { + const customFieldType = customFields?.find(f => f.name === key)?.type; + const isLocalizedCustomField = + customFieldType === 'localeString' || customFieldType === 'localeText'; + if (isLocalizedCustomField) { pathParts.push('customFields'); } pathParts.push(key); diff --git a/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts b/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts index e69922acf59..ed1aaf2fe1c 100644 --- a/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts +++ b/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts @@ -1517,6 +1517,197 @@ describe('OrderCalculator', () => { expect(order.subTotal).toBe(1350); }); }); + + describe('promotions which apply no discount', () => { + // Models the common "facet exclusions" setup: the conditions pass for the whole Order, + // but the action discounts only some of the items. + const discountExpensiveItemsAction = new PromotionItemAction({ + code: 'discount_expensive_items_action', + description: [{ languageCode: LanguageCode.en, value: '' }], + args: {}, + execute(ctx, orderLine) { + return 1000 <= orderLine.unitPrice ? -100 : 0; + }, + }); + + const neverDiscountAction = new PromotionItemAction({ + code: 'never_discount_action', + description: [{ languageCode: LanguageCode.en, value: '' }], + args: {}, + execute() { + return 0; + }, + }); + + const sideEffectAction = new PromotionItemAction({ + code: 'side_effect_action', + description: [{ languageCode: LanguageCode.en, value: '' }], + args: {}, + execute() { + return 0; + }, + // The benefit of this action is delivered by the side effect, not by a discount. + onActivate: () => undefined, + }); + + function createItemPromotion(action: PromotionItemAction, id = 1) { + return new Promotion({ + id, + name: `Test promotion ${id}`, + conditions: [{ code: alwaysTrueCondition.code, args: [] }], + promotionConditions: [alwaysTrueCondition], + actions: [{ code: action.code, args: [] }], + promotionActions: [action], + }); + } + + it('is not added to the Order', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 100, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [ + createItemPromotion(discountExpensiveItemsAction), + ]); + + expect(order.discounts.length).toBe(0); + expect(order.promotions).toEqual([]); + }); + + it('is added to the Order when it discounts at least one line', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 100, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + { + listPrice: 2000, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [ + createItemPromotion(discountExpensiveItemsAction), + ]); + + expect(order.discounts.length).toBe(1); + expect(order.promotions.map(p => p.id)).toEqual([1]); + }); + + it('is removed without removing the other Promotions on the Order', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 2000, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [ + createItemPromotion(discountExpensiveItemsAction, 1), + createItemPromotion(neverDiscountAction, 2), + ]); + + expect(order.discounts.length).toBe(1); + expect(order.promotions.map(p => p.id)).toEqual([1]); + }); + + it('is removed from the Order once it no longer discounts anything', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const promotion = createItemPromotion(discountExpensiveItemsAction); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 2000, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [promotion]); + expect(order.promotions.map(p => p.id)).toEqual([1]); + + order.lines[0].listPrice = 100; + await orderCalculator.applyPriceAdjustments(ctx, order, [promotion]); + + expect(order.discounts.length).toBe(0); + expect(order.promotions).toEqual([]); + }); + + it('is added to the Order when it only discounts shipping', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const promotion = new Promotion({ + id: 2, + name: 'Free shipping', + conditions: [{ code: alwaysTrueCondition.code, args: [] }], + promotionConditions: [alwaysTrueCondition], + actions: [{ code: freeShippingAction.code, args: [] }], + promotionActions: [freeShippingAction], + }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 100, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + order.shippingLines = [ + new ShippingLine({ + shippingMethodId: mockShippingMethodId, + adjustments: [], + }), + ]; + + await orderCalculator.applyPriceAdjustments(ctx, order, [promotion]); + + expect(order.shipping).toBe(0); + expect(order.promotions.map(p => p.id)).toEqual([2]); + }); + + it('is added to the Order when its action has a side effect', async () => { + const ctx = createRequestContext({ pricesIncludeTax: false }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 100, + taxCategory: taxCategoryStandard, + quantity: 1, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [ + createItemPromotion(sideEffectAction), + ]); + + expect(order.discounts.length).toBe(0); + expect(order.promotions.map(p => p.id)).toEqual([1]); + }); + }); }); describe('surcharges', () => { diff --git a/packages/core/src/service/helpers/order-calculator/order-calculator.ts b/packages/core/src/service/helpers/order-calculator/order-calculator.ts index 10234237f73..171fc906676 100644 --- a/packages/core/src/service/helpers/order-calculator/order-calculator.ts +++ b/packages/core/src/service/helpers/order-calculator/order-calculator.ts @@ -113,6 +113,7 @@ export class OrderCalculator { await this.applyShippingPromotions(ctx, order, promotions); } this.calculateOrderTotals(order); + this.removeIneffectivePromotions(order); return order; } @@ -387,4 +388,35 @@ export class OrderCalculator { order.promotions.push(promotion); } } + + /** + * Promotions are added to the Order as soon as their conditions pass, since a Promotion which + * discounts nothing on one OrderLine may still discount a later line, the Order total or the + * shipping. Once everything has been applied, any Promotion which ended up adjusting nothing + * at all is removed again: `Order.promotions` is what the usage limits are counted from, so a + * Promotion the customer got no benefit from must not consume a usage. + * + * Promotions whose actions define side effects are kept regardless, as their benefit (a free + * gift, say) is not expressed as an adjustment amount - and on the first pass the side effect + * has not run yet, so there is nothing to discount. + */ + private removeIneffectivePromotions(order: Order) { + if (!order.promotions?.length) { + return; + } + const adjustmentSources = new Set(); + for (const line of order.lines) { + for (const adjustment of line.adjustments ?? []) { + adjustmentSources.add(adjustment.adjustmentSource); + } + } + for (const shippingLine of order.shippingLines ?? []) { + for (const adjustment of shippingLine.adjustments ?? []) { + adjustmentSources.add(adjustment.adjustmentSource); + } + } + order.promotions = order.promotions.filter( + promotion => adjustmentSources.has(promotion.getSourceId()) || promotion.hasSideEffects, + ); + } } diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts index d8d79055460..a842f53041c 100644 --- a/packages/core/src/service/services/order.service.ts +++ b/packages/core/src/service/services/order.service.ts @@ -2469,6 +2469,7 @@ export class OrderService implements OnApplicationBootstrap { 'billingAddress', 'lines', 'shippingLines', + 'surcharges', 'aggregateOrder', 'sellerOrders', 'customer', diff --git a/packages/core/src/service/services/stock-level.service.spec.ts b/packages/core/src/service/services/stock-level.service.spec.ts index 89d6f3e3518..6ac4f371471 100644 --- a/packages/core/src/service/services/stock-level.service.spec.ts +++ b/packages/core/src/service/services/stock-level.service.spec.ts @@ -4,6 +4,7 @@ import { RequestContext } from '../../api/common/request-context'; import { RequestContextCacheService } from '../../cache/request-context-cache.service'; import { Channel } from '../../entity/channel/channel.entity'; import { StockLevel } from '../../entity/stock-level/stock-level.entity'; +import { StockLocation } from '../../entity/stock-location/stock-location.entity'; import { StockLevelService } from './stock-level.service'; @@ -31,8 +32,55 @@ const find = vi.fn(({ where }: any) => { return Promise.resolve(allStockLevels.filter(sl => ids.includes(String(sl.productVariantId)))); }); +/** + * The rows behind `getStockLevelsForVariant`, which selects the StockLocation relation and + * filters on the Channel. Variant 1 holds stock in two locations, so the grouping is tested + * against a variant with more than one row. + */ +const location1 = new StockLocation({ id: 1 }); +const location2 = new StockLocation({ id: 2 }); +const allStockLevelsWithLocation = [ + ...allStockLevels.map(sl => new StockLevel({ ...sl, stockLocation: location1 })), + new StockLevel({ + id: 6, + stockLocationId: 2, + stockLocation: location2, + stockOnHand: 7, + stockAllocated: 0, + productVariantId: 1, + }), +]; + +/** The bound parameters and where-clauses of each query-builder read, newest last. */ +const queryBuilderReads: Array<{ clauses: string[]; params: Record }> = []; + +function createQueryBuilder() { + const read = { clauses: [] as string[], params: {} as Record }; + const record = (clause: string, params: Record) => { + read.clauses.push(clause); + Object.assign(read.params, params); + return queryBuilder; + }; + const queryBuilder: any = { + leftJoinAndSelect: () => queryBuilder, + leftJoin: () => queryBuilder, + where: record, + andWhere: record, + getMany: () => { + queryBuilderReads.push(read); + const ids: Array = read.params.productVariantIds; + return Promise.resolve( + allStockLevelsWithLocation.filter(sl => + ids.map(id => String(id)).includes(String(sl.productVariantId)), + ), + ); + }, + }; + return queryBuilder; +} + const mockConnection = { - getRepository: () => ({ find }), + getRepository: () => ({ find, createQueryBuilder }), } as any; // Sums the levels it is given, as DefaultStockLocationStrategy does, so the assertions are @@ -48,10 +96,10 @@ const mockConfigService = { }, } as any; -function newCtx(): RequestContext { +function newCtx(channelId = 1): RequestContext { return new RequestContext({ apiType: 'shop', - channel: new Channel({ id: 1 }), + channel: new Channel({ id: channelId }), authorizedAsOwnerOnly: false, isAuthorized: true, session: {} as any, @@ -64,6 +112,7 @@ describe('StockLevelService', () => { beforeEach(() => { find.mockClear(); + queryBuilderReads.length = 0; ctx = newCtx(); service = new StockLevelService( mockConnection, @@ -118,4 +167,79 @@ describe('StockLevelService', () => { expect(find).toHaveBeenCalledTimes(2); }); }); + + // The Admin API `ProductVariant.stockLevels` field, which is channel-filtered and therefore + // reads through its own loader rather than the one behind `getAvailableStock`. + describe('getStockLevelsForVariant', () => { + it('batches concurrent lookups into a single query', async () => { + const results = await Promise.all( + [2, 3, 4, 5].map(id => service.getStockLevelsForVariant(ctx, id)), + ); + + expect(queryBuilderReads.length).toBe(1); + expect(queryBuilderReads[0].clauses).toContain( + 'stockLevel.productVariantId IN (:...productVariantIds)', + ); + expect(queryBuilderReads[0].params.productVariantIds).toEqual([2, 3, 4, 5]); + expect(results.map(levels => levels.map(sl => sl.productVariantId))).toEqual([ + [2], + [3], + [4], + [5], + ]); + }); + + it('groups every row of a batch onto the variant it belongs to', async () => { + const [variant1, variant2] = await Promise.all([ + service.getStockLevelsForVariant(ctx, 1), + service.getStockLevelsForVariant(ctx, 2), + ]); + + expect(queryBuilderReads.length).toBe(1); + expect(variant1.map(sl => sl.stockLocationId)).toEqual([1, 2]); + expect(variant2.map(sl => sl.stockLocationId)).toEqual([1]); + }); + + it('selects the StockLocation relation, as the unbatched query did', async () => { + const [stockLevel] = await service.getStockLevelsForVariant(ctx, 2); + + expect(stockLevel.stockLocation).toBe(location1); + }); + + it('returns an empty array for a variant with no StockLevel rows', async () => { + const result = await service.getStockLevelsForVariant(ctx, 99); + + expect(result).toEqual([]); + }); + + it('filters on the Channel of the RequestContext', async () => { + await service.getStockLevelsForVariant(newCtx(42), 1); + + expect(queryBuilderReads[0].clauses).toContain('channel.id = :channelId'); + expect(queryBuilderReads[0].params.channelId).toBe(42); + }); + + it('re-reads on a later tick, so levels changed within a request are not stale', async () => { + await service.getStockLevelsForVariant(ctx, 1); + await service.getStockLevelsForVariant(ctx, 1); + + expect(queryBuilderReads.length).toBe(2); + }); + + it('does not share a batch across RequestContexts', async () => { + await Promise.all([ + service.getStockLevelsForVariant(ctx, 1), + service.getStockLevelsForVariant(newCtx(), 2), + ]); + + expect(queryBuilderReads.length).toBe(2); + }); + + it('does not share a batch with getAvailableStock', async () => { + await Promise.all([service.getStockLevelsForVariant(ctx, 1), service.getAvailableStock(ctx, 1)]); + + expect(queryBuilderReads.length).toBe(1); + expect(find).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/core/src/service/services/stock-level.service.ts b/packages/core/src/service/services/stock-level.service.ts index 5dc635f73d1..0c1eb7a9a94 100644 --- a/packages/core/src/service/services/stock-level.service.ts +++ b/packages/core/src/service/services/stock-level.service.ts @@ -58,14 +58,7 @@ export class StockLevelService { } async getStockLevelsForVariant(ctx: RequestContext, productVariantId: ID): Promise { - return this.connection - .getRepository(ctx, StockLevel) - .createQueryBuilder('stockLevel') - .leftJoinAndSelect('stockLevel.stockLocation', 'stockLocation') - .leftJoin('stockLocation.channels', 'channel') - .where('stockLevel.productVariantId = :productVariantId', { productVariantId }) - .andWhere('channel.id = :channelId', { channelId: ctx.channelId }) - .getMany(); + return this.getStockLevelsForVariantLoader(ctx).load(productVariantId); } /** @@ -96,12 +89,56 @@ export class StockLevelService { } private async batchLoadStockLevels(ctx: RequestContext, ids: ID[]): Promise { - const uniqueIds = [...new Map(ids.map(id => [String(id), id])).values()]; const stockLevels = await this.connection.getRepository(ctx, StockLevel).find({ where: { - productVariantId: In(uniqueIds), + productVariantId: In(this.uniqueIds(ids)), }, }); + return this.groupByVariantId(ids, stockLevels); + } + + /** + * The Admin API `ProductVariant.stockLevels` field is resolved once per variant, so listing + * variants costs one channel-filtered query per row. The loader is held on the RequestContext + * and the channel filter below is read from that same ctx, so one loader per ctx cannot mix + * channels. `cache: false` for the same reason as {@link getStockLevelLoader}: batch, but do + * not memoize, so a write earlier in the request is not masked. + */ + private getStockLevelsForVariantLoader(ctx: RequestContext): DataLoader { + return this.requestCache.get( + ctx, + 'StockLevelService.stockLevelsForVariant', + () => + new DataLoader( + ids => this.batchLoadStockLevelsForVariant(ctx, ids as ID[]), + { cache: false }, + ), + ); + } + + private async batchLoadStockLevelsForVariant(ctx: RequestContext, ids: ID[]): Promise { + const stockLevels = await this.connection + .getRepository(ctx, StockLevel) + .createQueryBuilder('stockLevel') + .leftJoinAndSelect('stockLevel.stockLocation', 'stockLocation') + .leftJoin('stockLocation.channels', 'channel') + .where('stockLevel.productVariantId IN (:...productVariantIds)', { + productVariantIds: this.uniqueIds(ids), + }) + .andWhere('channel.id = :channelId', { channelId: ctx.channelId }) + .getMany(); + return this.groupByVariantId(ids, stockLevels); + } + + private uniqueIds(ids: ID[]): ID[] { + return [...new Map(ids.map(id => [String(id), id])).values()]; + } + + /** + * Returns one entry per requested id, in the order requested, as a DataLoader batch function + * must. A variant with no rows gets an empty array. + */ + private groupByVariantId(ids: ID[], stockLevels: StockLevel[]): StockLevel[][] { const byVariantId = new Map(); for (const stockLevel of stockLevels) { const key = String(stockLevel.productVariantId); diff --git a/packages/create/package.json b/packages/create/package.json index 533c1895681..0e04c99bb96 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -45,7 +45,7 @@ "cross-spawn": "^7.0.6", "fs-extra": "^11.2.0", "handlebars": "^4.7.9", - "open": "^10.2.0", + "open": "^11.0.1", "picocolors": "^1.1.1", "semver": "^7.7.2", "tar": "^7.4.3" diff --git a/packages/dashboard/e2e/fixtures/e2e-shared-config.ts b/packages/dashboard/e2e/fixtures/e2e-shared-config.ts index dd9ad692860..66c0c8108c8 100644 --- a/packages/dashboard/e2e/fixtures/e2e-shared-config.ts +++ b/packages/dashboard/e2e/fixtures/e2e-shared-config.ts @@ -73,6 +73,13 @@ export const e2eCustomFields: CustomFields = { label: [{ languageCode: LanguageCode.en, value: 'SEO Description' }], ui: { tab: 'SEO', fullWidth: true }, }, + // Deliberately has no label, so the Dashboard has to fall back to the + // field's own name when rendering it (#5246). + { + name: 'seoKeywords', + type: 'localeString', + ui: { tab: 'SEO' }, + }, // ── Details tab ── { name: 'detailNotes', diff --git a/packages/dashboard/e2e/tests/catalog/custom-fields.spec.ts b/packages/dashboard/e2e/tests/catalog/custom-fields.spec.ts index f4b83240fbc..b765e1baca7 100644 --- a/packages/dashboard/e2e/tests/catalog/custom-fields.spec.ts +++ b/packages/dashboard/e2e/tests/catalog/custom-fields.spec.ts @@ -159,6 +159,18 @@ test.describe('Custom Fields', () => { await expect(dp.formItem('Weight').getByRole('spinbutton')).toBeVisible(); }); + // #5246 — a localized custom field with no label falls back to the field's own + // name, not the internal form path (which also changes with the content language). + test('should label an unlabelled locale custom field with its field name', async ({ page }) => { + await goToFirstProduct(page); + + await page.locator('[data-slot="tabs-trigger"]', { hasText: 'SEO' }).click(); + + const labels = page.locator('[data-slot="field-label"]'); + await expect(labels.getByText('seoKeywords', { exact: true })).toBeVisible(); + await expect(labels.filter({ hasText: 'customFields.seoKeywords' })).toHaveCount(0); + }); + test('should render locale fields with language selector', async ({ page }) => { await goToFirstProduct(page); diff --git a/packages/dashboard/e2e/tests/catalog/product-list.spec.ts b/packages/dashboard/e2e/tests/catalog/product-list.spec.ts index 86ea2081b5b..58b4aa52792 100644 --- a/packages/dashboard/e2e/tests/catalog/product-list.spec.ts +++ b/packages/dashboard/e2e/tests/catalog/product-list.spec.ts @@ -353,6 +353,47 @@ test.describe('Product List', () => { ).toBeVisible(); }); + // #5272 — verifies that product search and column filters combine with AND semantics + test('should keep column filters applied when a search term is entered', async ({ page }) => { + const lp = listPage(page); + await lp.goto(); + await lp.expectLoaded(); + + const initialCount = await lp.getRows().count(); + + // Filter down to the 3 products with "Camera" in the name + await lp.openAddFilterMenu(); + const dropdown = page.locator('[data-slot="dropdown-menu-content"]'); + await dropdown.getByRole('menuitem', { name: /name/i }).click(); + + const dialog = page.locator('[role="dialog"]'); + await dialog.getByPlaceholder('Enter filter value...').fill('Camera'); + const filterResponse = page.waitForResponse( + resp => resp.url().includes('/admin-api') && resp.status() === 200, + ); + await dialog.getByRole('button', { name: 'Apply filter' }).click(); + await filterResponse; + await lp.expectRowCount(3); + + // Searching within that filter must AND the two, not OR them + await lp.search('Lens'); + await lp.expectRowCount(1); + await expect(lp.getRows().filter({ hasText: 'Camera Lens' })).toHaveCount(1); + await expect(lp.getRows().filter({ hasText: 'Instant Camera' })).toHaveCount(0); + + // Clearing the search must restore the filtered set + await lp.clearSearch(); + await lp.expectRowCount(3); + + // Clearing the filter must restore the initial set + const clearResponse = page.waitForResponse( + resp => resp.url().includes('/admin-api') && resp.status() === 200, + ); + await page.getByRole('button', { name: 'Clear all' }).click(); + await clearResponse; + await lp.expectRowCount(initialCount); + }); + test('should clear an applied filter via the clear all button', async ({ page }) => { const lp = listPage(page); await lp.goto(); diff --git a/packages/dashboard/e2e/tests/sales/orders.spec.ts b/packages/dashboard/e2e/tests/sales/orders.spec.ts index 28abb99f186..aba0f7d6669 100644 --- a/packages/dashboard/e2e/tests/sales/orders.spec.ts +++ b/packages/dashboard/e2e/tests/sales/orders.spec.ts @@ -156,6 +156,131 @@ test.describe('Orders', () => { await expect(page).not.toHaveURL(/\/draft\//); }); + // #5253 — eligible shipping methods must refresh after a line is added, without + // a page reload. + test('should refresh eligible shipping methods after adding a line to an existing draft order', async ({ + page, + }) => { + test.setTimeout(60_000); // Draft order flow involves multiple mutations + + const client = new VendureAdminClient(page); + await client.login(); + const { countries } = await client.gql( + `query { countries(options: { take: 1 }) { items { code } } }`, + ); + const customerSuffix = Date.now(); + const customerName = `Shipping Refresh ${customerSuffix}`; + const { createCustomer } = await client.gql( + `mutation ($input: CreateCustomerInput!) { + createCustomer(input: $input) { + ... on Customer { id } + ... on ErrorResult { errorCode message } + } + }`, + { + input: { + firstName: 'Shipping', + lastName: `Refresh ${customerSuffix}`, + emailAddress: `shipping.refresh.${customerSuffix}@test.com`, + }, + }, + ); + await client.gql( + `mutation ($customerId: ID!, $input: CreateAddressInput!) { + createCustomerAddress(customerId: $customerId, input: $input) { id } + }`, + { + customerId: createCustomer.id, + input: { + fullName: customerName, + streetLine1: '5253 Shipping Lane', + city: 'Testville', + postalCode: 'T3 5ST', + countryCode: countries.items[0].code, + defaultShippingAddress: true, + }, + }, + ); + // Both seeded methods accept an empty order, so this test needs one that does + // not. It is created and deleted here rather than reconfiguring a seeded method, + // because spec files run in parallel against one shared server. + const conditionalMethodName = `Minimum Order Shipping ${customerSuffix}`; + const { createShippingMethod } = await client.gql( + `mutation ($input: CreateShippingMethodInput!) { + createShippingMethod(input: $input) { id } + }`, + { + input: { + code: `minimum-order-shipping-${customerSuffix}`, + fulfillmentHandler: 'manual-fulfillment', + checker: { + code: 'default-shipping-eligibility-checker', + arguments: [{ name: 'orderMinimum', value: '1' }], + }, + calculator: { + code: 'default-shipping-calculator', + arguments: [ + { name: 'rate', value: '750' }, + { name: 'taxRate', value: '0' }, + { name: 'includesTax', value: 'auto' }, + ], + }, + translations: [{ languageCode: 'en', name: conditionalMethodName, description: '' }], + }, + }, + ); + + let draftCreated = false; + try { + const lp = listPage(page); + await lp.goto(); + await lp.expectLoaded(); + await lp.newButton.click(); + await expect(page).toHaveURL(/\/orders\/draft\//, { timeout: 10_000 }); + draftCreated = true; + + // Set a customer with a default shipping address first — this + // enables the eligible-shipping-methods query for the first time + // (still a zero-line order). + await page.getByRole('button', { name: /Select customer/i }).click(); + await page.getByPlaceholder('Search customers...').fill(String(customerSuffix)); + const customerOption = page.getByRole('option').filter({ hasText: customerName }); + await expect(customerOption).toBeVisible({ timeout: 5_000 }); + const eligibilityResponse = page.waitForResponse( + response => + response.url().includes('/admin-api') && + response.status() === 200 && + (response.request().postData() ?? '').includes('DraftOrderEligibleShippingMethods'), + ); + await customerOption.click(); + await eligibilityResponse; + await expect(page.getByText('Standard Shipping', { exact: true })).toBeVisible(); + await expect(page.getByText(conditionalMethodName, { exact: true })).toHaveCount(0); + + // Adding a product makes the minimum-order method eligible. + const addItemButton = page.locator('[role="combobox"]').filter({ hasText: 'Add item to order' }); + await addItemButton.scrollIntoViewIfNeeded(); + await addItemButton.click(); + await page.getByPlaceholder('Add item to order...').fill('laptop'); + await expect(page.getByRole('option').first()).toBeVisible({ timeout: 5_000 }); + await page.getByRole('option').first().click(); + + const shippingLabel = page.getByText(conditionalMethodName, { exact: true }); + await expect(shippingLabel).toBeVisible({ timeout: 10_000 }); + await expect(page.getByText('No shipping methods available')).toHaveCount(0); + } finally { + try { + if (draftCreated) { + await deleteCurrentDraft(page); + } + } finally { + await client.gql(`mutation ($id: ID!) { deleteShippingMethod(id: $id) { result message } }`, { + id: createShippingMethod.id, + }); + } + } + }); + test('should show the completed order in the list', async ({ page }) => { const lp = listPage(page); await lp.goto(); @@ -212,11 +337,7 @@ test.describe('Orders', () => { await expect(page).toHaveURL(/\/orders\/draft\//, { timeout: 10_000 }); // Delete the draft without configuring it - await page.getByRole('button', { name: /Delete draft/i }).click(); - // Confirm the deletion dialog — AlertDialog uses "Continue" as the action button - await page.locator('[role="alertdialog"]').getByRole('button', { name: 'Continue' }).click(); - // Should navigate back to the orders list (URL may include query params) - await expect(page).not.toHaveURL(/\/draft\//, { timeout: 15_000 }); + await deleteCurrentDraft(page); await expect(page.getByTestId('page-heading')).toBeVisible(); }); @@ -829,6 +950,17 @@ async function createFulfilledOrder(client: VendureAdminClient): Promise return orderId; } +/** + * Deletes the draft order currently open in the page. A test may leave a popover or + * combobox open over the page, so dismiss that first or the delete button is not clickable. + */ +async function deleteCurrentDraft(page: Page) { + await page.keyboard.press('Escape'); + await page.getByRole('button', { name: /Delete draft/i }).click(); + await page.locator('[role="alertdialog"]').getByRole('button', { name: 'Continue' }).click(); + await expect(page).not.toHaveURL(/\/draft\//, { timeout: 15_000 }); +} + /** * Creates a payment method (idempotent), builds an order via the * Admin API, and returns the order ID. diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 7eae4a6165f..5298e17b4f9 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -113,7 +113,7 @@ "graphql": "^16.11.0", "input-otp": "^1.4.2", "json-edit-react": "^1.23.1", - "lucide-react": "^0.475.0", + "lucide-react": "^0.577.0", "motion": "^12.38.0", "next-themes": "^0.4.6", "react": "^19.2.4", diff --git a/packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx b/packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx index 7758a2e58b1..83f85c3d4b7 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx @@ -25,20 +25,14 @@ function AdministratorListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - firstName: { contains: searchTerm }, - lastName: { contains: searchTerm }, - emailAddress: { contains: searchTerm }, + _or: [ + { firstName: { contains: searchTerm } }, + { lastName: { contains: searchTerm } }, + { emailAddress: { contains: searchTerm } }, + ], } : {}; }} - transformVariables={variables => { - return { - options: { - ...variables.options, - filterOperator: 'OR', - }, - }; - }} additionalColumns={{ name: { meta: { dependencies: ['id', 'firstName', 'lastName'] }, diff --git a/packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx b/packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx index 8c373bb1ab8..8eab7042d08 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx @@ -28,20 +28,10 @@ function CountryListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - name: { contains: searchTerm }, - code: { contains: searchTerm }, + _or: [{ name: { contains: searchTerm } }, { code: { contains: searchTerm } }], } : {}; }} - transformVariables={variables => { - return { - ...variables, - options: { - ...variables.options, - filterOperator: 'OR', - }, - }; - }} customizeColumns={{ name: { cell: ({ row }) => , diff --git a/packages/dashboard/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx b/packages/dashboard/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx index a697feff78b..c5fc53e6b82 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx @@ -93,12 +93,10 @@ export function CustomerGroupMembersTable({ }} onSearchTermChange={searchTerm => { return { - lastName: { - contains: searchTerm, - }, - emailAddress: { - contains: searchTerm, - }, + _or: [ + { lastName: { contains: searchTerm } }, + { emailAddress: { contains: searchTerm } }, + ], }; }} additionalColumns={{ diff --git a/packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx b/packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx index 83ef9d696ed..c0d333d15bb 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx @@ -24,20 +24,14 @@ function CustomerListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - lastName: { contains: searchTerm }, - emailAddress: { contains: searchTerm }, - phoneNumber: { contains: searchTerm }, + _or: [ + { lastName: { contains: searchTerm } }, + { emailAddress: { contains: searchTerm } }, + { phoneNumber: { contains: searchTerm } }, + ], } : {}; }} - transformVariables={variables => { - return { - options: { - ...variables.options, - filterOperator: 'OR', - }, - }; - }} route={Route} customizeColumns={{ user: { diff --git a/packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx b/packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx index 47198ea0d26..a436be6b40b 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx @@ -6,6 +6,7 @@ import { Button } from '@/vdb/components/ui/button.js'; import { Form } from '@/vdb/components/ui/form.js'; import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js'; import { useGeneratedForm } from '@/vdb/framework/form-engine/use-generated-form.js'; +import { ActionBarItem } from '@/vdb/framework/layout-engine/action-bar-item-wrapper.js'; import { Page, PageActionBar, @@ -13,20 +14,19 @@ import { PageLayout, PageTitle, } from '@/vdb/framework/layout-engine/page-layout.js'; -import { ActionBarItem } from '@/vdb/framework/layout-engine/action-bar-item-wrapper.js'; import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js'; import { api } from '@/vdb/graphql/api.js'; import { Trans, useLingui } from '@lingui/react/macro'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; import { ResultOf } from 'gql.tada'; import { User } from 'lucide-react'; import { toast } from 'sonner'; +import { addressFragment } from '../_customers/customers.graphql.js'; import { CustomerAddressSelector } from './components/customer-address-selector.js'; import { DraftOrderStatus } from './components/draft-order-status.js'; import { EditOrderTable } from './components/edit-order-table.js'; import { OrderAddress } from './components/order-address.js'; -import { addressFragment } from '../_customers/customers.graphql.js'; import { addItemToDraftOrderDocument, adjustDraftOrderLineDocument, @@ -58,6 +58,7 @@ function DraftOrderPage() { const params = Route.useParams(); const { t } = useLingui(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const { entity, refreshEntity, form } = useDetailPage({ queryDocument: addCustomFields(orderDetailDocument, { @@ -72,6 +73,13 @@ function DraftOrderPage() { params: { id: params.id }, }); + // Shipping eligibility is recomputed server-side from the whole order, so any + // change to the order makes the cached quote list invalid. + const refreshOrderAndShippingMethods = () => { + refreshEntity(); + void queryClient.invalidateQueries({ queryKey: ['eligibleShippingMethods', entity?.id] }); + }; + const { form: orderCustomFieldsForm } = useGeneratedForm({ document: setDraftOrderCustomFieldsDocument, varName: undefined, @@ -91,7 +99,7 @@ function DraftOrderPage() { mutationFn: api.mutate(setDraftOrderCustomFieldsDocument), onSuccess: (result: ResultOf) => { toast.success(t`Order custom fields updated`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to update order custom fields: ${error.message}`); @@ -102,6 +110,10 @@ function DraftOrderPage() { queryKey: ['eligibleShippingMethods', entity?.id], queryFn: () => api.query(draftOrderEligibleShippingMethodsDocument, { orderId: entity?.id ?? '' }), enabled: !!entity?.shippingAddress?.streetLine1, + // The key carries the order id, so the app-wide keepPreviousData default would + // show one order's shipping methods on another. An explicit undefined is needed + // to override that default; omitting the option is not the same thing. + placeholderData: undefined, }); const { mutate: addItemToDraftOrder } = useMutation({ @@ -111,7 +123,7 @@ function DraftOrderPage() { switch (order.__typename) { case 'Order': toast.success(t`Item added to order`); - refreshEntity(); + refreshOrderAndShippingMethods(); break; default: toast.error(order.message); @@ -132,7 +144,7 @@ function DraftOrderPage() { switch (order.__typename) { case 'Order': toast.success(t`Order line updated`); - refreshEntity(); + refreshOrderAndShippingMethods(); break; default: toast.error(order.message); @@ -151,7 +163,7 @@ function DraftOrderPage() { switch (order.__typename) { case 'Order': toast.success(t`Order line removed`); - refreshEntity(); + refreshOrderAndShippingMethods(); break; default: toast.error(order.message); @@ -181,12 +193,8 @@ function DraftOrderPage() { }); addresses = customer?.addresses ?? []; } - const defaultShippingAddress = addresses.find( - address => address.defaultShippingAddress, - ); - const defaultBillingAddress = addresses.find( - address => address.defaultBillingAddress, - ); + const defaultShippingAddress = addresses.find(address => address.defaultShippingAddress); + const defaultBillingAddress = addresses.find(address => address.defaultBillingAddress); // Sequence the address mutations: they all mutate the same // version-tracked Order, so firing them concurrently makes // the second read a stale version and fail with an @@ -214,9 +222,11 @@ function DraftOrderPage() { }); } } catch (e) { - toast.error(t`Failed to set address for order: ${e instanceof Error ? e.message : String(e)}`); + toast.error( + t`Failed to set address for order: ${e instanceof Error ? e.message : String(e)}`, + ); } - refreshEntity(); + refreshOrderAndShippingMethods(); break; } default: @@ -233,7 +243,7 @@ function DraftOrderPage() { mutationFn: api.mutate(setShippingAddressForDraftOrderDocument), onSuccess: (result: ResultOf) => { toast.success(t`Shipping address set for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to set shipping address for order: ${error.message}`); @@ -244,7 +254,7 @@ function DraftOrderPage() { mutationFn: api.mutate(setBillingAddressForDraftOrderDocument), onSuccess: (result: ResultOf) => { toast.success(t`Billing address set for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to set billing address for order: ${error.message}`); @@ -255,7 +265,7 @@ function DraftOrderPage() { mutationFn: api.mutate(unsetShippingAddressForDraftOrderDocument), onSuccess: (result: ResultOf) => { toast.success(t`Shipping address unset for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to unset shipping address for order: ${error.message}`); @@ -266,7 +276,7 @@ function DraftOrderPage() { mutationFn: api.mutate(unsetBillingAddressForDraftOrderDocument), onSuccess: (result: ResultOf) => { toast.success(t`Billing address unset for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to unset billing address for order: ${error.message}`); @@ -280,7 +290,7 @@ function DraftOrderPage() { switch (order.__typename) { case 'Order': toast.success(t`Shipping method set for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); break; default: toast.error(order.message); @@ -299,7 +309,7 @@ function DraftOrderPage() { switch (order.__typename) { case 'Order': toast.success(t`Coupon code set for order`); - refreshEntity(); + refreshOrderAndShippingMethods(); break; default: toast.error(order.message); @@ -315,7 +325,7 @@ function DraftOrderPage() { mutationFn: api.mutate(removeCouponCodeFromDraftOrderDocument), onSuccess: (result: ResultOf) => { toast.success(t`Coupon code removed from order`); - refreshEntity(); + refreshOrderAndShippingMethods(); }, onError: error => { toast.error(t`Failed to remove coupon code from order: ${error.message}`); @@ -329,7 +339,7 @@ function DraftOrderPage() { switch (order?.__typename) { case 'Order': toast.success(t`Draft order completed`); - refreshEntity(); + refreshOrderAndShippingMethods(); setTimeout(() => { navigate({ to: `/orders/$id`, params: { id: order.id } }); }, 500); @@ -496,7 +506,11 @@ function DraftOrderPage() { Customer}> {entity?.customer?.id ? ( - diff --git a/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.spec.ts b/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.spec.ts new file mode 100644 index 00000000000..5b23a520f95 --- /dev/null +++ b/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.spec.ts @@ -0,0 +1,159 @@ +import { addDetailQueryDocument } from '@/vdb/framework/form-engine/custom-form-component-extensions.js'; +import { globalRegistry } from '@/vdb/framework/registry/global-registry.js'; +import { QueryClient } from '@tanstack/react-query'; +import { parse, print } from 'graphql'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { loadModifyingOrder, loadRegularOrder, loadSellerOrder } from './order-detail-loaders.js'; + +const { query } = vi.hoisted(() => ({ + query: vi.fn(), +})); + +vi.mock('@/vdb/graphql/api.js', () => ({ + api: { query }, +})); + +describe('loadRegularOrder', () => { + beforeEach(() => { + globalRegistry.get('detailQueryDocumentRegistry').clear(); + query.mockReset(); + }); + + it('includes registered order detail query extensions in the preloaded query', async () => { + addDetailQueryDocument( + 'order-detail', + parse(` + query GetOrder($id: ID!) { + order(id: $id) { + channels { + defaultLanguageCode + } + } + } + `), + ); + query.mockResolvedValue({ + order: { + code: 'T_1', + state: 'PaymentSettled', + }, + }); + const context = { + queryClient: { + ensureQueryData: vi.fn(options => options.queryFn()), + }, + }; + + await loadRegularOrder(context, { id: '1' }); + + expect(print(query.mock.calls[0][0])).toContain('defaultLanguageCode'); + }); + + it('includes registered seller order detail query extensions in the preloaded query', async () => { + addDetailQueryDocument( + 'seller-order-detail', + parse(` + query GetOrder($id: ID!) { + order(id: $id) { + channels { + defaultLanguageCode + } + } + } + `), + ); + query.mockResolvedValue({ + order: { + aggregateOrder: { + code: 'T_1', + id: 'aggregate-order-1', + }, + code: 'T_2', + state: 'PaymentSettled', + }, + }); + const context = { + queryClient: { + ensureQueryData: vi.fn(options => options.queryFn()), + }, + }; + + await loadSellerOrder(context, { + aggregateOrderId: 'aggregate-order-1', + sellerOrderId: 'seller-order-1', + }); + + expect(print(query.mock.calls[0][0])).toContain('defaultLanguageCode'); + }); + + it('includes registered order modification query extensions in the preloaded query', async () => { + addDetailQueryDocument( + 'order-modify', + parse(` + query GetOrder($id: ID!) { + order(id: $id) { + channels { + defaultLanguageCode + } + } + } + `), + ); + query.mockResolvedValue({ + order: { + code: 'T_1', + state: 'Modifying', + }, + }); + const context = { + queryClient: new QueryClient(), + }; + + await loadModifyingOrder(context, { id: '1' }); + + expect(print(query.mock.calls[0][0])).toContain('defaultLanguageCode'); + }); + + it('does not reuse an incompatible order detail preload after redirecting to modification', async () => { + addDetailQueryDocument( + 'order-detail', + parse(` + query GetOrder($id: ID!) { + order(id: $id) { + channels { + code + } + } + } + `), + ); + addDetailQueryDocument( + 'order-modify', + parse(` + query GetOrder($id: ID!) { + order(id: $id) { + channels { + defaultLanguageCode + } + } + } + `), + ); + query.mockResolvedValue({ + order: { + code: 'T_1', + state: 'Modifying', + }, + }); + const context = { + queryClient: new QueryClient(), + }; + + await expect(loadRegularOrder(context, { id: '1' })).rejects.toBeDefined(); + await loadModifyingOrder(context, { id: '1' }); + + expect(query).toHaveBeenCalledTimes(2); + expect(print(query.mock.calls[1][0])).toContain('defaultLanguageCode'); + }); +}); diff --git a/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx b/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx index 3a23575a7d9..2c9a4608f42 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx @@ -1,3 +1,4 @@ +import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js'; import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js'; import { getDetailQueryOptions } from '@/vdb/framework/page/use-detail-page.js'; import { ResultOf } from '@/vdb/graphql/graphql.js'; @@ -6,16 +7,22 @@ import { redirect } from '@tanstack/react-router'; import { OrderDetail } from '../components/order-detail-shared.js'; import { orderDetailDocument } from '../orders.graphql.js'; -async function ensureOrderWithIdExists(context: any, params: { id: string }): Promise { +async function ensureOrderWithIdExists( + context: any, + params: { id: string }, + pageId = 'order-detail', +): Promise { if (!params.id) { throw new Error('ID param is required'); } + const { extendedQuery } = extendDetailFormQuery( + addCustomFields(orderDetailDocument, { includeNestedFragments: ['OrderLine', 'Fulfillment'] }), + pageId, + ); + const result: ResultOf = await context.queryClient.ensureQueryData( - getDetailQueryOptions( - addCustomFields(orderDetailDocument, { includeNestedFragments: ['OrderLine', 'Fulfillment'] }), - { id: params.id }, - ), + getDetailQueryOptions(extendedQuery, { id: params.id }), ); if (!result.order) { @@ -24,10 +31,22 @@ async function ensureOrderWithIdExists(context: any, params: { id: string }): Pr return result.order; } -export async function commonRegularOrderLoader(context: any, params: { id: string }): Promise { - const order = await ensureOrderWithIdExists(context, params); +function clearPreloadedOrder(context: any, id: string) { + context.queryClient.removeQueries({ + queryKey: getDetailQueryOptions(orderDetailDocument, { id }).queryKey, + exact: true, + }); +} + +export async function commonRegularOrderLoader( + context: any, + params: { id: string }, + pageId = 'order-detail', +): Promise { + const order = await ensureOrderWithIdExists(context, params, pageId); if (order.state === 'Draft') { + clearPreloadedOrder(context, params.id); throw redirect({ to: `/orders/draft/${params.id}`, }); @@ -39,6 +58,7 @@ export async function loadRegularOrder(context: any, params: { id: string }) { const order = await commonRegularOrderLoader(context, params); if (order.state === 'Modifying') { + clearPreloadedOrder(context, params.id); throw redirect({ to: `/orders/${params.id}/modify`, }); @@ -53,6 +73,7 @@ export async function loadDraftOrder(context: any, params: { id: string }) { const order = await ensureOrderWithIdExists(context, params); if (order.state !== 'Draft') { + clearPreloadedOrder(context, params.id); throw redirect({ to: `/orders/${params.id}`, }); @@ -64,8 +85,9 @@ export async function loadDraftOrder(context: any, params: { id: string }) { } export async function loadModifyingOrder(context: any, params: { id: string }) { - const order = await commonRegularOrderLoader(context, params); + const order = await commonRegularOrderLoader(context, params, 'order-modify'); if (order.state !== 'Modifying') { + clearPreloadedOrder(context, params.id); throw redirect({ to: `/orders/${params.id}`, }); @@ -88,11 +110,13 @@ export async function loadSellerOrder( throw new Error('Both seller order ID and aggregate order ID params are required'); } + const { extendedQuery } = extendDetailFormQuery( + addCustomFields(orderDetailDocument, { includeNestedFragments: ['OrderLine', 'Fulfillment'] }), + 'seller-order-detail', + ); + const result: ResultOf = await context.queryClient.ensureQueryData( - getDetailQueryOptions( - addCustomFields(orderDetailDocument, { includeNestedFragments: ['OrderLine', 'Fulfillment'] }), - { id: params.sellerOrderId }, - ), + getDetailQueryOptions(extendedQuery, { id: params.sellerOrderId }), ); if (!result.order) { @@ -112,12 +136,14 @@ export async function loadSellerOrder( } if (result.order.state === 'Draft') { + clearPreloadedOrder(context, params.sellerOrderId); throw redirect({ to: `/orders/draft/${params.sellerOrderId}`, }); } if (result.order.state === 'Modifying') { + clearPreloadedOrder(context, params.sellerOrderId); throw redirect({ to: `/orders/${params.sellerOrderId}/modify`, }); diff --git a/packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx b/packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx index 1f546330005..61b92bb9744 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.tsx @@ -88,19 +88,10 @@ function ProductListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - name: { contains: searchTerm }, - sku: { contains: searchTerm }, + _or: [{ name: { contains: searchTerm } }, { sku: { contains: searchTerm } }], } : {}; }} - transformVariables={variables => { - return { - options: { - ...variables.options, - filterOperator: 'OR', - }, - }; - }} route={Route} > ); diff --git a/packages/dashboard/src/app/routes/_authenticated/_products/products.tsx b/packages/dashboard/src/app/routes/_authenticated/_products/products.tsx index 5486e86214d..47b43342156 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_products/products.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_products/products.tsx @@ -56,9 +56,11 @@ function ProductListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - name: { contains: searchTerm }, - slug: { contains: searchTerm }, - sku: { contains: searchTerm }, + _or: [ + { name: { contains: searchTerm } }, + { slug: { contains: searchTerm } }, + { sku: { contains: searchTerm } }, + ], } : {}; }} @@ -77,14 +79,6 @@ function ProductListPage() { component: FacetValueFacetedFilter, }, }} - transformVariables={variables => { - return { - options: { - ...variables.options, - filterOperator: 'OR', - }, - }; - }} defaultSort={[{ id: 'updatedAt', desc: true }]} defaultVisibility={{ name: true, diff --git a/packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx b/packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx index 778b01c9b0f..60f9ef2e964 100644 --- a/packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx +++ b/packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx @@ -38,19 +38,10 @@ function PromotionListPage() { onSearchTermChange={searchTerm => { return searchTerm ? { - name: { contains: searchTerm }, - couponCode: { contains: searchTerm }, + _or: [{ name: { contains: searchTerm } }, { couponCode: { contains: searchTerm } }], } : {}; }} - transformVariables={variables => { - return { - options: { - ...variables.options, - filterOperator: 'OR' as const, - }, - }; - }} customizeColumns={{ name: { cell: ({ row }) => , diff --git a/packages/dashboard/src/i18n/locales/ne.po b/packages/dashboard/src/i18n/locales/ne.po index dfc930ab8eb..052b5052549 100644 --- a/packages/dashboard/src/i18n/locales/ne.po +++ b/packages/dashboard/src/i18n/locales/ne.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "राज्य" +msgstr "अवस्था" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/nl.po b/packages/dashboard/src/i18n/locales/nl.po index 7cf65bbd5bb..173973c64b3 100644 --- a/packages/dashboard/src/i18n/locales/nl.po +++ b/packages/dashboard/src/i18n/locales/nl.po @@ -4871,7 +4871,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "Staat" +msgstr "Status" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/ru.po b/packages/dashboard/src/i18n/locales/ru.po index 76fe3db8357..9cdc815616e 100644 --- a/packages/dashboard/src/i18n/locales/ru.po +++ b/packages/dashboard/src/i18n/locales/ru.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "Регион" +msgstr "Состояние" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/sv.po b/packages/dashboard/src/i18n/locales/sv.po index e67aad7f217..f4e65722a12 100644 --- a/packages/dashboard/src/i18n/locales/sv.po +++ b/packages/dashboard/src/i18n/locales/sv.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "Region" +msgstr "Tillstånd" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/tr.po b/packages/dashboard/src/i18n/locales/tr.po index 170f353737e..c97aa1de2a5 100644 --- a/packages/dashboard/src/i18n/locales/tr.po +++ b/packages/dashboard/src/i18n/locales/tr.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "İl" +msgstr "Durum" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/uk.po b/packages/dashboard/src/i18n/locales/uk.po index d985f9179b3..7a92f99cc0f 100644 --- a/packages/dashboard/src/i18n/locales/uk.po +++ b/packages/dashboard/src/i18n/locales/uk.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "Регіон" +msgstr "Стан" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/zh_Hans.po b/packages/dashboard/src/i18n/locales/zh_Hans.po index d2cd06e80fe..881b7abba12 100644 --- a/packages/dashboard/src/i18n/locales/zh_Hans.po +++ b/packages/dashboard/src/i18n/locales/zh_Hans.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "州/省" +msgstr "状态" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/i18n/locales/zh_Hant.po b/packages/dashboard/src/i18n/locales/zh_Hant.po index 937656eca27..48e0448677b 100644 --- a/packages/dashboard/src/i18n/locales/zh_Hant.po +++ b/packages/dashboard/src/i18n/locales/zh_Hant.po @@ -4880,7 +4880,7 @@ msgstr "" #: src/app/routes/_authenticated/_orders/orders.tsx #: src/app/routes/_authenticated/_system/job-queue.tsx msgid "State" -msgstr "州/省" +msgstr "狀態" #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx #: src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx diff --git a/packages/dashboard/src/lib/components/data-table/use-generated-columns.spec.tsx b/packages/dashboard/src/lib/components/data-table/use-generated-columns.spec.tsx new file mode 100644 index 00000000000..7eb3a948c06 --- /dev/null +++ b/packages/dashboard/src/lib/components/data-table/use-generated-columns.spec.tsx @@ -0,0 +1,137 @@ +import { + defineDashboardExtension, + executeDashboardExtensionCallbacks, +} from '@/vdb/framework/extension-api/define-dashboard-extension.js'; +import { addDisplayComponent } from '@/vdb/framework/extension-api/display-component-extensions.js'; +import { PageBlockContext } from '@/vdb/framework/layout-engine/page-block-provider.js'; +import { PageContext } from '@/vdb/framework/layout-engine/page-provider.js'; +import { CellContext, flexRender } from '@tanstack/react-table'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import { useGeneratedColumns } from './use-generated-columns.js'; + +// The display component registry is a module-level Map with no removal API, so an entry +// registered by one test stays visible to every later test. Each test therefore uses its +// own pageId; reusing one would let an earlier registration decide a later result. +const BLOCK_ID = 'test-block'; + +const PRICE = 1234; + +const fields = [ + { name: 'sku', type: 'String', nullable: false, list: false, isPaginatedList: false, isScalar: true }, + { name: 'price', type: 'Int', nullable: false, list: false, isPaginatedList: false, isScalar: true }, +]; + +// Mirrors product-variants-table.tsx, which supplies a `cell` for `price` but not for `sku`. +// That asymmetry is why the documented example works on `sku` and silently fails on `price`. +const customizeColumns = { + price: { cell: () => core-money-cell }, +} as any; + +const cellContext = { + cell: { getValue: () => PRICE }, + row: { original: { sku: 'SKU-1', price: PRICE } }, +} as unknown as CellContext; + +/** + * Renders one generated column's cell the way TanStack renders it. `flexRender` calls + * `createElement` for any function cell, so a cell using hooks behaves here as it does in + * a real table; calling `column.cell(context)` directly would not. + */ +function renderColumnCell(pageId: string, columnId: string): string { + const captured: { columns?: Array<{ id?: string; cell?: any }> } = {}; + + function Harness() { + const { columns } = useGeneratedColumns({ + fields, + customizeColumns, + includeSelectionColumn: false, + includeActionsColumn: false, + }); + captured.columns = columns as any; + return null; + } + + renderToStaticMarkup( + + + + + , + ); + + const column = captured.columns?.find(c => c.id === columnId); + if (!column?.cell) { + throw new Error(`Column "${columnId}" was not generated`); + } + return renderToStaticMarkup(<>{flexRender(column.cell, cellContext)}); +} + +describe('useGeneratedColumns display component precedence', () => { + it('gives precedence to a registered display component over a core-supplied cell function', () => { + const pageId = 'test-page-registered'; + + addDisplayComponent({ + pageId, + blockId: BLOCK_ID, + field: 'price', + component: () => registered-display-component, + }); + + expect(renderColumnCell(pageId, 'price')).toBe('registered-display-component'); + }); + + it('passes the cell value to a display component that overrides a core-supplied cell', () => { + const pageId = 'test-page-value'; + + addDisplayComponent({ + pageId, + blockId: BLOCK_ID, + field: 'price', + component: ({ value }) => {`value:${value as number}`}, + }); + + expect(renderColumnCell(pageId, 'price')).toBe(`value:${PRICE}`); + }); + + it('falls back to the core-supplied cell function when no display component is registered', () => { + expect(renderColumnCell('test-page-unregistered', 'price')).toBe('core-money-cell'); + }); + + it('still renders a registered display component on a column with no core-supplied cell', () => { + const pageId = 'test-page-no-core-cell'; + + addDisplayComponent({ + pageId, + blockId: BLOCK_ID, + field: 'sku', + component: () => sku-display-component, + }); + + expect(renderColumnCell(pageId, 'sku')).toBe('sku-display-component'); + }); + + it('renders the default display component when a column has neither a core cell nor a registration', () => { + expect(renderColumnCell('test-page-default', 'sku')).toBe(String(PRICE)); + }); + + it('applies a display component registered through defineDashboardExtension', () => { + // The route real extensions take. registerDataTableExtensions defaults blockId to + // 'list-table', so the block has to be named explicitly to target this table. + const pageId = 'test-page-extension-api'; + + defineDashboardExtension({ + dataTables: [ + { + pageId, + blockId: BLOCK_ID, + displayComponents: [{ column: 'price', component: () => via-extension-api }], + }, + ], + }); + executeDashboardExtensionCallbacks(); + + expect(renderColumnCell(pageId, 'price')).toBe('via-extension-api'); + }); +}); diff --git a/packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx b/packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx index a81eb8899a9..9432cf16736 100644 --- a/packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx +++ b/packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx @@ -138,12 +138,24 @@ export function useGeneratedColumns>({ pageId && pageBlock?.blockId ? generateDisplayComponentKey(pageId, pageBlock.blockId, fieldInfo.name) : undefined; + // A component registered via addDisplayComponent() takes precedence over a + // core-supplied `cell` function (e.g. the Money cell on price columns). Without + // this, registering an override for such a column silently does nothing. + // + // The registry is read here rather than at cell-render time, so a column keeps + // whichever renderer was chosen when this memo last ran. Extensions register + // during executeDashboardExtensionCallbacks(), which completes before any data + // table mounts, so the answer is settled by the time a column is generated. + const registeredDisplayComponent = displayComponentId + ? getDisplayComponent(displayComponentId) + : undefined; - // If a custom cell function is provided, use it directly (like additionalColumns does). - // This preserves the same behavior and prevents cell unmounting issues. - // Only use CellWrapper for columns without custom cells. + // Where nothing is registered, a custom cell function is used directly rather + // than through CellWrapper, which is what keeps the cell from unmounting on + // every table re-render. Everything else goes through CellWrapper, which is the + // only place the display component registry is consulted. const cellFn = - typeof customCell === 'function' + typeof customCell === 'function' && !registeredDisplayComponent ? customCell : (cellContext: CellContext) => ( >({ } return { columns: finalColumns, customFieldColumnNames }; - }, [fields, customizeColumns, rowActions, deleteMutation, additionalColumns, defaultColumnOrder]); + // `pageId` and `pageBlock?.blockId` are dependencies because they form the display + // component registry key, so a column generated under one page or block must not be + // reused under another. + }, [ + fields, + customizeColumns, + rowActions, + deleteMutation, + additionalColumns, + defaultColumnOrder, + pageId, + pageBlock?.blockId, + ]); return { columns, customFieldColumnNames }; } diff --git a/packages/dashboard/src/lib/components/shared/custom-fields-form.tsx b/packages/dashboard/src/lib/components/shared/custom-fields-form.tsx index 1324a665166..ca8001c9af2 100644 --- a/packages/dashboard/src/lib/components/shared/custom-fields-form.tsx +++ b/packages/dashboard/src/lib/components/shared/custom-fields-form.tsx @@ -249,7 +249,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, disabled }: Readonly {localeFallbackPlaceholder @@ -275,7 +275,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, disabled }: Readonly @@ -302,7 +302,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, disabled }: Readonly @@ -353,7 +353,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, disabled }: Readonly @@ -369,7 +369,13 @@ interface CustomFieldFormItemProps { getTranslation: ( input: string | Array<{ languageCode: string; value: string }> | null | undefined, ) => string | undefined; - fieldName: string; + /** + * The form control's path. The element id is built from this rather than from + * the field's own name, because a page can render this form more than once — + * the product variant page renders it per price — and the field's name would + * then repeat down the page. + */ + fieldPath: string; fieldState?: ControllerFieldState; children: React.ReactNode; } @@ -377,14 +383,14 @@ interface CustomFieldFormItemProps { function CustomFieldFormItem({ fieldDef, getTranslation, - fieldName, + fieldPath, fieldState, children, }: Readonly) { - const fieldId = `field-${fieldName}`; + const fieldId = `field-${fieldPath}`; return ( - {getTranslation(fieldDef.label) ?? fieldName} + {getTranslation(fieldDef.label) ?? fieldDef.name} {children} {getTranslation(fieldDef.description) && ( {getTranslation(fieldDef.description)} diff --git a/packages/dashboard/src/lib/framework/page/list-page.stories.tsx b/packages/dashboard/src/lib/framework/page/list-page.stories.tsx index c531810f939..9cd71cbd4df 100644 --- a/packages/dashboard/src/lib/framework/page/list-page.stories.tsx +++ b/packages/dashboard/src/lib/framework/page/list-page.stories.tsx @@ -148,21 +148,7 @@ export const WithSearch: Story = { }, onSearchTermChange: (searchTerm: string) => { return { - name: { - contains: searchTerm, - }, - code: { - contains: searchTerm, - }, - }; - }, - transformVariables: (variables: any) => { - return { - ...variables, - options: { - ...variables.options, - filterOperator: 'OR', - }, + _or: [{ name: { contains: searchTerm } }, { code: { contains: searchTerm } }], }; }, customizeColumns: { @@ -222,21 +208,7 @@ export const Complete: Story = { }, onSearchTermChange: (searchTerm: string) => { return { - name: { - contains: searchTerm, - }, - code: { - contains: searchTerm, - }, - }; - }, - transformVariables: (variables: any) => { - return { - ...variables, - options: { - ...variables.options, - filterOperator: 'OR', - }, + _or: [{ name: { contains: searchTerm } }, { code: { contains: searchTerm } }], }; }, customizeColumns: { diff --git a/packages/dashboard/src/lib/framework/page/list-page.tsx b/packages/dashboard/src/lib/framework/page/list-page.tsx index 491ee47bd92..dc5664061af 100644 --- a/packages/dashboard/src/lib/framework/page/list-page.tsx +++ b/packages/dashboard/src/lib/framework/page/list-page.tsx @@ -140,6 +140,11 @@ export interface ListPageProps< * Allows you to customize how the search term is used in the list query options. * For instance, when you want the term to filter on specific fields. * + * The returned filter is merged with the column & faceted filters, so when the term should + * match any of several fields, group those fields in an `_or` block. Setting a page-level + * `filterOperator: 'OR'` would instead OR together _every_ top-level condition, including + * the column filters. + * * @example * ```tsx * { * return { - * firstName: { contains: searchTerm }, - * lastName: { contains: searchTerm }, - * emailAddress: { contains: searchTerm }, + * _or: [ + * { firstName: { contains: searchTerm } }, + * { lastName: { contains: searchTerm } }, + * { emailAddress: { contains: searchTerm } }, + * ], * }; * }} * /> diff --git a/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/package.json b/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/package.json new file mode 100644 index 00000000000..ad64dbfb896 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/package.json @@ -0,0 +1,5 @@ +{ + "type": "module", + "name": "my-plugin", + "version": "9.9.9" +} diff --git a/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/src/my.plugin.ts b/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/src/my.plugin.ts new file mode 100644 index 00000000000..f247ef2e376 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/src/my.plugin.ts @@ -0,0 +1,12 @@ +import { PluginCommonModule, VendurePlugin } from '@vendure/core'; + +import pkg from '../package.json'; + +@VendurePlugin({ + imports: [PluginCommonModule], + providers: [], + dashboard: './dashboard/index.tsx', +}) +export class MyPlugin { + static version = pkg.version; +} diff --git a/packages/dashboard/vite/tests/fixtures-json-pkg/package.json b/packages/dashboard/vite/tests/fixtures-json-pkg/package.json new file mode 100644 index 00000000000..f52a970ef79 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json-pkg/package.json @@ -0,0 +1,6 @@ +{ + "type": "commonjs", + "name": "json-pkg", + "version": "0.0.1", + "main": "index.ts" +} diff --git a/packages/dashboard/vite/tests/fixtures-json-pkg/type-only-vendure-config.ts b/packages/dashboard/vite/tests/fixtures-json-pkg/type-only-vendure-config.ts new file mode 100644 index 00000000000..6c8520a9d69 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json-pkg/type-only-vendure-config.ts @@ -0,0 +1,16 @@ +import type { VendureConfig } from '@vendure/core'; + +import type packageMetadata from './package.json'; +import { type name } from './package.json'; + +export { type version } from './package.json'; + +export const config: VendureConfig = { + apiOptions: { port: 3000 }, + authOptions: { tokenMethod: 'bearer' }, + dbConnectionOptions: { type: 'postgres' }, + paymentOptions: { paymentMethodHandlers: [] }, + plugins: [], +}; + +export type FixturePackageMetadata = typeof packageMetadata & { name: typeof name }; diff --git a/packages/dashboard/vite/tests/fixtures-json-pkg/vendure-config.ts b/packages/dashboard/vite/tests/fixtures-json-pkg/vendure-config.ts new file mode 100644 index 00000000000..595bf7da184 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json-pkg/vendure-config.ts @@ -0,0 +1,11 @@ +import { VendureConfig } from '@vendure/core'; + +import { MyPlugin } from './my-plugin/src/my.plugin.js'; + +export const config: VendureConfig = { + apiOptions: { port: 3000 }, + authOptions: { tokenMethod: 'bearer' }, + dbConnectionOptions: { type: 'postgres' }, + paymentOptions: { paymentMethodHandlers: [] }, + plugins: [MyPlugin], +}; diff --git a/packages/dashboard/vite/tests/fixtures-json/config-data.json b/packages/dashboard/vite/tests/fixtures-json/config-data.json new file mode 100644 index 00000000000..8c850a5fd2f --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json/config-data.json @@ -0,0 +1 @@ +{ "foo": "bar" } diff --git a/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/my.plugin.ts b/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/my.plugin.ts new file mode 100644 index 00000000000..7678a69d327 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/my.plugin.ts @@ -0,0 +1,12 @@ +import { PluginCommonModule, VendurePlugin } from '@vendure/core'; + +import pluginData from './plugin-data.json'; + +@VendurePlugin({ + imports: [PluginCommonModule], + providers: [], + dashboard: './dashboard/index.tsx', +}) +export class MyPlugin { + static sheetId = pluginData.sheetId; +} diff --git a/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/plugin-data.json b/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/plugin-data.json new file mode 100644 index 00000000000..7f29335683b --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json/my-plugin/src/plugin-data.json @@ -0,0 +1 @@ +{ "sheetId": "abc123" } diff --git a/packages/dashboard/vite/tests/fixtures-json/package.json b/packages/dashboard/vite/tests/fixtures-json/package.json new file mode 100644 index 00000000000..9243be09fa1 --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json/package.json @@ -0,0 +1,6 @@ +{ + "type": "commonjs", + "name": "json", + "version": "0.0.1", + "main": "index.ts" +} diff --git a/packages/dashboard/vite/tests/fixtures-json/vendure-config.ts b/packages/dashboard/vite/tests/fixtures-json/vendure-config.ts new file mode 100644 index 00000000000..4014826b07a --- /dev/null +++ b/packages/dashboard/vite/tests/fixtures-json/vendure-config.ts @@ -0,0 +1,13 @@ +import { VendureConfig } from '@vendure/core'; + +import configData from './config-data.json'; +import { MyPlugin } from './my-plugin/src/my.plugin.js'; + +export const config: VendureConfig = { + apiOptions: { port: 3000 }, + authOptions: { tokenMethod: 'bearer' }, + dbConnectionOptions: { type: 'postgres' }, + paymentOptions: { paymentMethodHandlers: [] }, + plugins: [MyPlugin], + customFields: { Product: [{ name: configData.foo, type: 'string' }] }, +}; diff --git a/packages/dashboard/vite/tests/json-imports.spec.ts b/packages/dashboard/vite/tests/json-imports.spec.ts new file mode 100644 index 00000000000..6594396a9bb --- /dev/null +++ b/packages/dashboard/vite/tests/json-imports.spec.ts @@ -0,0 +1,112 @@ +import { readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +import { compile } from '../utils/compiler.js'; +import { debugLogger, noopLogger } from '../utils/logger.js'; + +// #4807 — a config or plugin which imports a .json file must still compile. The +// compiler copies the JSON into the output alongside the emitted .js files, so +// the require/import in the emitted config resolves. +describe('compiling a config which imports .json files', () => { + async function compileFixture(module: 'commonjs' | 'esm') { + const tempDir = join(__dirname, `./__temp/json-${module}`); + await rm(tempDir, { recursive: true, force: true }); + const result = await compile({ + outputPath: tempDir, + vendureConfigPath: join(__dirname, 'fixtures-json', 'vendure-config.ts'), + logger: process.env.LOG ? debugLogger : noopLogger, + module, + }); + return { tempDir, result }; + } + + it('should load a config which imports JSON in commonjs mode', { timeout: 60_000 }, async () => { + const { tempDir, result } = await compileFixture('commonjs'); + + // The JSON imported directly by the config, and the one imported + // transitively by the plugin, both have to reach the output. + expect(JSON.parse(await readFile(join(tempDir, 'config-data.json'), 'utf-8'))).toEqual({ + foo: 'bar', + }); + expect( + JSON.parse(await readFile(join(tempDir, 'my-plugin', 'src', 'plugin-data.json'), 'utf-8')), + ).toEqual({ sheetId: 'abc123' }); + + // Reading the value back off the loaded config proves the emitted + // require() resolved, rather than only that the file was copied. + expect(result.vendureConfig.customFields?.Product?.[0].name).toBe('bar'); + }); + + // ESM mode emits a bare `import data from './x.json'`, which Node rejects with + // ERR_IMPORT_ATTRIBUTE_MISSING because it wants `with { type: 'json' }`. Copying + // the file is therefore necessary but not sufficient there, so this asserts only + // the copying. Loading is not asserted: it succeeds under Vitest, whose loader + // resolves JSON without the attribute, and would pass here while still failing + // for a real `module: 'esm'` project. + it('should copy imported JSON into the output in esm mode', { timeout: 60_000 }, async () => { + const { tempDir } = await compileFixture('esm'); + + expect(JSON.parse(await readFile(join(tempDir, 'config-data.json'), 'utf-8'))).toEqual({ + foo: 'bar', + }); + expect( + JSON.parse(await readFile(join(tempDir, 'my-plugin', 'src', 'plugin-data.json'), 'utf-8')), + ).toEqual({ sheetId: 'abc123' }); + }); + // Skipping the package.json silently leaves the import in the emitted output, so + // the config fails to load with a bare "Cannot find module". Say so up front. + it('should warn when a package.json import is skipped', { timeout: 60_000 }, async () => { + const tempDir = join(__dirname, './__temp/json-pkg-warn'); + await rm(tempDir, { recursive: true, force: true }); + const warnings: string[] = []; + + await compile({ + outputPath: tempDir, + vendureConfigPath: join(__dirname, 'fixtures-json-pkg', 'vendure-config.ts'), + logger: { ...noopLogger, warn: (message: string) => warnings.push(message) }, + module: 'commonjs', + }).catch(() => undefined); + + const packageJsonWarning = warnings.find(w => w.includes('package.json')); + expect(packageJsonWarning).toBeDefined(); + expect(packageJsonWarning).toContain(join('my-plugin', 'package.json')); + }); + + it('should not warn when package.json is imported only for types', { timeout: 60_000 }, async () => { + const tempDir = join(__dirname, './__temp/json-pkg-types'); + await rm(tempDir, { recursive: true, force: true }); + const warnings: string[] = []; + + await compile({ + outputPath: tempDir, + vendureConfigPath: join(__dirname, 'fixtures-json-pkg', 'type-only-vendure-config.ts'), + logger: { ...noopLogger, warn: (message: string) => warnings.push(message) }, + module: 'commonjs', + }); + + expect(warnings.some(warning => warning.includes('package.json cannot be copied'))).toBe(false); + }); + + // A package.json reached through the import graph must not be copied. In a + // nested directory its "type" field decides how the compiled .js files beside + // it are loaded, so copying it makes those files fail to load. + it('should not copy a package.json into the output', { timeout: 60_000 }, async () => { + const tempDir = join(__dirname, './__temp/json-pkg'); + await rm(tempDir, { recursive: true, force: true }); + + await compile({ + outputPath: tempDir, + vendureConfigPath: join(__dirname, 'fixtures-json-pkg', 'vendure-config.ts'), + logger: process.env.LOG ? debugLogger : noopLogger, + module: 'commonjs', + }).catch(() => undefined); + + await expect(readFile(join(tempDir, 'my-plugin', 'package.json'), 'utf-8')).rejects.toThrow(); + // The output root keeps the generated stub, not the project's own file. + expect(JSON.parse(await readFile(join(tempDir, 'package.json'), 'utf-8'))).toEqual({ + type: 'commonjs', + private: true, + }); + }); +}); diff --git a/packages/dashboard/vite/utils/compiler.ts b/packages/dashboard/vite/utils/compiler.ts index 05f483dad29..1250efebfb7 100644 --- a/packages/dashboard/vite/utils/compiler.ts +++ b/packages/dashboard/vite/utils/compiler.ts @@ -296,9 +296,23 @@ async function resolveSourceContext( // Collect all local source files by walking the import tree const collectStart = Date.now(); - const sourceFiles = await collectLocalSourceFiles(inputPath, originalTsConfigInfo); + const { sourceFiles, skippedPackageJsonFiles } = await collectLocalSourceFiles( + inputPath, + originalTsConfigInfo, + ); logger.debug(`Collected ${sourceFiles.length} source files in ${Date.now() - collectStart}ms`); + // The import stays in the emitted JavaScript, so leaving this silent means the + // config fails to load with a bare "Cannot find module" naming a file the author + // can see on disk. + if (skippedPackageJsonFiles.length) { + logger.warn( + `A package.json cannot be copied into the compiled config output, because its "type" field ` + + `decides how the compiled files beside it are loaded. Loading the config will fail on ` + + `${skippedPackageJsonFiles.join(', ')}. Read the file at runtime instead of importing it.`, + ); + } + let sourceRoot = customSourceRoot; if (!sourceRoot) { sourceRoot = resolveDefaultSourceRoot( @@ -325,12 +339,14 @@ async function resolveSourceContext( } /** - * Compiles TypeScript files to JavaScript using per-file transpilation. + * Compiles TypeScript files to JavaScript using per-file transpilation, and copies + * across the JSON they import. * * Instead of using `ts.createProgram()` (which resolves all imports including * node_modules type definitions and can OOM on projects with heavy dependencies), * this function: - * 1. Transpiles each given source file individually with `ts.transpileModule()` (no type resolution) + * 1. Transpiles each given TypeScript file individually with `ts.transpileModule()` + * (no type resolution), and copies JSON through unchanged * 2. Validates every emit lands inside outputPath before writing anything * * This avoids loading massive `.d.ts` files from packages like `openai`, `ai`, etc. @@ -353,6 +369,21 @@ async function compileTypeScript({ }): Promise { await fs.ensureDir(outputPath); + // Copying the JSON is enough for CommonJS, whose require() reads it directly. + // ESM emits the import unchanged, and Node refuses to load a JSON module + // without an `with { type: 'json' }` attribute that TypeScript will not emit + // for a source file targeting CommonJS. Say so here, naming the files, so the + // ERR_IMPORT_ATTRIBUTE_MISSING that follows has an explanation above it. + if (module === 'esm') { + const jsonFiles = sourceFiles.filter(file => file.endsWith('.json')); + if (jsonFiles.length) { + logger.warn( + `Imported JSON is not yet supported with module: 'esm', so loading the config will fail on ` + + `${jsonFiles.join(', ')}. Use module: 'commonjs', or read the file at runtime instead of importing it.`, + ); + } + } + // Build path transformer for ESM mode // This is necessary because tsconfig-paths.register() only works for CommonJS require(), // not for ESM import(). We need to transform the import paths during transpilation. @@ -395,17 +426,22 @@ async function compileTypeScript({ for (const filePath of sourceFiles) { const content = await fs.readFile(filePath, 'utf-8'); - const result = ts.transpileModule(content, { - compilerOptions, - fileName: filePath, - transformers, - }); + // JSON is data, not code: it is copied byte for byte so the require() or + // import in the emitted JavaScript resolves against the same content the + // server would have read. + const outputText = filePath.endsWith('.json') + ? content + : ts.transpileModule(content, { + compilerOptions, + fileName: filePath, + transformers, + }).outputText; // Compute output path preserving directory structure relative to source root const relativePath = path.relative(sourceRoot, filePath); const outputFilePath = path.join(outputPath, relativePath).replace(/\.tsx?$/, '.js'); - pendingEmits.push({ sourceFile: filePath, outputFilePath, outputText: result.outputText }); + pendingEmits.push({ sourceFile: filePath, outputFilePath, outputText }); } for (const emit of pendingEmits) { @@ -436,8 +472,9 @@ function assertWithinOutputPath(outputFilePath: string, outputPath: string): voi /** * Collects all local source files reachable from the entry point by following - * import/export declarations. Only follows local imports (relative paths and - * tsconfig path aliases), not npm package imports. + * import/export declarations, along with the JSON files they import. Only follows + * local imports (relative paths and tsconfig path aliases), not npm package + * imports. * * This is intentionally lightweight — it uses TypeScript's AST parser only * for reading import statements, without any module resolution or type loading. @@ -445,8 +482,9 @@ function assertWithinOutputPath(outputFilePath: string, outputPath: string): voi async function collectLocalSourceFiles( entryFile: string, tsConfigInfo?: { baseUrl: string; paths: Record }, -): Promise { +): Promise<{ sourceFiles: string[]; skippedPackageJsonFiles: string[] }> { const visited = new Set(); + const skippedPackageJson = new Set(); async function processFile(filePath: string) { const resolved = await resolveSourceFile(filePath); @@ -455,11 +493,23 @@ async function collectLocalSourceFiles( // Skip declaration files and non-source files before adding to visited, // so they don't leak into the returned sourceFiles array. if (resolved.endsWith('.d.ts') || resolved.endsWith('.d.tsx')) return; - if (!/\.(ts|tsx|js|jsx)$/.test(resolved)) return; + if (!/\.(ts|tsx|js|jsx|json)$/.test(resolved)) return; + // A package.json is never copied. At the output root it would collide with + // the one written there to declare the module type, and in any nested + // directory its own "type" field decides how the compiled .js files beside + // it are loaded, which breaks them. + if (path.basename(resolved) === 'package.json') { + skippedPackageJson.add(resolved); + return; + } if (visited.has(resolved)) return; visited.add(resolved); + // JSON is copied verbatim and declares no imports, so there is nothing to + // parse or follow. + if (resolved.endsWith('.json')) return; + const content = await fs.readFile(resolved, 'utf-8'); const sf = ts.createSourceFile(resolved, content, ts.ScriptTarget.Latest, true); const dir = path.dirname(resolved); @@ -467,6 +517,7 @@ async function collectLocalSourceFiles( ts.forEachChild(sf, node => { if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) return; + if (isTypeOnlyImportOrExport(node)) return; const moduleSpecifier = node.moduleSpecifier; if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) return; const importPath = moduleSpecifier.text; @@ -489,7 +540,37 @@ async function collectLocalSourceFiles( } await processFile(entryFile); - return [...visited]; + return { sourceFiles: [...visited], skippedPackageJsonFiles: [...skippedPackageJson] }; +} + +function isTypeOnlyImportOrExport(node: ts.ImportDeclaration | ts.ExportDeclaration) { + if (ts.isImportDeclaration(node)) { + const importClause = node.importClause; + if (!importClause) { + return false; + } + if (importClause.isTypeOnly) { + return true; + } + const namedBindings = importClause.namedBindings; + return ( + !importClause.name && + !!namedBindings && + ts.isNamedImports(namedBindings) && + namedBindings.elements.length > 0 && + namedBindings.elements.every(element => element.isTypeOnly) + ); + } + + if (node.isTypeOnly) { + return true; + } + return ( + !!node.exportClause && + ts.isNamedExports(node.exportClause) && + node.exportClause.elements.length > 0 && + node.exportClause.elements.every(element => element.isTypeOnly) + ); } async function registerTsConfigPaths(options: {