From 9ad8b0c635d8f3b7395f30808f2df72e0b2d96bc Mon Sep 17 00:00:00 2001 From: thomslbua <95583576+thomslbua@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:53:29 +0200 Subject: [PATCH 01/70] fix(core): prevent orphan-nullification of surcharges in applyPriceAdjustments --- packages/core/src/service/services/order.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts index 0d39d56c131..b458e8ff1eb 100644 --- a/packages/core/src/service/services/order.service.ts +++ b/packages/core/src/service/services/order.service.ts @@ -2439,6 +2439,7 @@ export class OrderService implements OnApplicationBootstrap { 'billingAddress', 'lines', 'shippingLines', + 'surcharges', 'aggregateOrder', 'sellerOrders', 'customer', From 78c8c136a193f9bce0379a324805647b3f112a5f Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Tue, 25 Aug 2026 11:07:59 +0200 Subject: [PATCH 02/70] fix(core): warn when migration patterns stop matching `runMigrations()` returns an empty array both when every migration has already been applied and when the configured `migrations` globs matched no files. The CLI derives its report from that array alone, so the second case is presented as `No pending migrations found` and `vendure migrate --run` exits 0 having applied nothing. Reporting it directly does not work: `log()` is a no-op whenever `VENDURE_RUNNING_IN_CLI` is set, which is why the condition is invisible today. Throwing would break the `create` scaffold, which configures a migrations glob before any migration file exists. So the condition is handed to the caller through a new `RunMigrationsOptions.onDiagnostic` callback. Zero loaded migration classes on its own is not evidence of a problem, it is also what a project looks like before its first migration is authored. The discriminator is the `migrations` table: zero classes loaded while the database has migrations on record can only mean the patterns stopped matching. That is also the dangerous case, a deploy pointed at `dist/migrations/*.js` that was never built. The schema-drift warning goes through the same callback. It was already computed on every `vendure migrate --run` and then discarded through the suppressed `log()`, and it is the check the issue actually asks for. The callback receives data rather than a pre-formatted string, both CLI entry points build their report from one function, and a warning no longer renders with a green success glyph. Based on the diagnosis and the callback seam from #5039 by @alex-hahn. Relates to #5001 --- packages/cli/e2e/migrate-command.e2e-spec.ts | 28 +++- packages/cli/src/commands/migrate/migrate.ts | 7 +- .../commands/migrate/migration-operations.ts | 25 ++- .../commands/migrate/migration-report.spec.ts | 59 +++++++ .../src/commands/migrate/migration-report.ts | 38 +++++ .../migrate/run-migration/run-migration.ts | 19 ++- packages/core/src/index.ts | 8 +- packages/core/src/migrate.ts | 153 ++++++++++++++++-- 8 files changed, 308 insertions(+), 29 deletions(-) create mode 100644 packages/cli/src/commands/migrate/migration-report.spec.ts create mode 100644 packages/cli/src/commands/migrate/migration-report.ts diff --git a/packages/cli/e2e/migrate-command.e2e-spec.ts b/packages/cli/e2e/migrate-command.e2e-spec.ts index 4a0dddf7559..d2bcd8a45a7 100644 --- a/packages/cli/e2e/migrate-command.e2e-spec.ts +++ b/packages/cli/e2e/migrate-command.e2e-spec.ts @@ -226,6 +226,29 @@ 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'); + }); + it('should handle database connection errors gracefully', async () => { process.chdir(TEST_PROJECT_DIR); @@ -252,9 +275,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/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..48d86d05272 --- /dev/null +++ b/packages/cli/src/commands/migrate/migration-report.spec.ts @@ -0,0 +1,59 @@ +import { MigrationDiagnostic } from '@vendure/core'; +import { describe, expect, it } from 'vitest'; + +import { buildMigrationReport } from './migration-report'; + +const unmatched: MigrationDiagnostic = { + type: 'no-migrations-matched', + patterns: ['dist/migrations/*.js'], + cwd: '/project', +}; +const outOfSync: MigrationDiagnostic = { + type: 'schema-out-of-sync', + queries: ['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.details).toContain('/project'); + 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 migrations'); + expect(report.message).toContain('Successfully ran 1 migrations'); + 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); + }); + + 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..b9861067512 --- /dev/null +++ b/packages/cli/src/commands/migrate/migration-report.ts @@ -0,0 +1,38 @@ +import { describeDiagnostic, 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.type === 'no-migrations-matched'); + const summary = migrationsRan.length + ? `Successfully ran ${migrationsRan.length} migrations` + : patternsUnmatched + ? 'No migration files could be loaded' + : 'No pending migrations found'; + + const details = diagnostics.map(diagnostic => describeDiagnostic(diagnostic).join('\n')).join('\n\n'); + return { + summary, + details, + message: details ? `${summary}\n\n${details}` : summary, + hasWarnings: diagnostics.length > 0, + }; +} 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/src/index.ts b/packages/core/src/index.ts index 7ba1b021055..bb5236375a3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,7 +18,13 @@ export * from './event-bus/index'; export * from './health-check/index'; export * from './i18n/index'; export * from './job-queue/index'; -export { generateMigration, revertLastMigration, runMigrations } from './migrate'; +export { describeDiagnostic, generateMigration, revertLastMigration, runMigrations } from './migrate'; +export type { + MigrationDiagnostic, + NoMigrationsMatchedDiagnostic, + RunMigrationsOptions, + SchemaOutOfSyncDiagnostic, +} from './migrate'; export * from './migration-utils/index'; export * from './plugin/index'; export * from './process-context/index'; diff --git a/packages/core/src/migrate.ts b/packages/core/src/migrate.ts index 66fc0913a7c..2d332ee91f8 100644 --- a/packages/core/src/migrate.ts +++ b/packages/core/src/migrate.ts @@ -2,7 +2,7 @@ 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'; @@ -30,6 +30,73 @@ export interface MigrationOptions { outputDir?: string; } +/** + * @description + * 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. + * + * @docsCategory migration + * @since 3.7.2 + */ +export interface NoMigrationsMatchedDiagnostic { + type: 'no-migrations-matched'; + /** + * @description + * The glob patterns configured in `dbConnectionOptions.migrations`. + */ + patterns: string[]; + /** + * @description + * The directory the patterns were resolved against. + */ + cwd: string; +} + +/** + * @description + * The database schema does not match the current entity configuration, so a new migration + * needs to be generated. + * + * @docsCategory migration + * @since 3.7.2 + */ +export interface SchemaOutOfSyncDiagnostic { + type: 'schema-out-of-sync'; + /** + * @description + * The SQL statements which would bring the schema back into line with the configuration. + */ + queries: string[]; +} + +/** + * @description + * A condition detected while running migrations which cannot be inferred from the return value + * of {@link runMigrations}. + * + * @docsCategory migration + * @since 3.7.2 + */ +export type MigrationDiagnostic = NoMigrationsMatchedDiagnostic | SchemaOutOfSyncDiagnostic; + +/** + * @description + * Options for {@link runMigrations}. + * + * @docsCategory migration + * @since 3.7.2 + */ +export interface RunMigrationsOptions { + /** + * @description + * Invoked for each {@link MigrationDiagnostic} detected during the run. These conditions are + * printed to the console by default, but that output is suppressed when running from the + * Vendure CLI so that the CLI can render them itself. + */ + onDiagnostic?: (diagnostic: MigrationDiagnostic) => void; +} + /** * @description * Runs any pending database migrations. See [TypeORM migration docs](https://typeorm.io/#/migrations) @@ -37,11 +104,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(describeDiagnostic(diagnostic).join('\n'))); + }; try { + const unmatched = await detectUnmatchedPatterns(connection); + if (unmatched) { + report(unmatched); + } const migrations = await disableForeignKeysForSqLite(connection, () => connection.runMigrations({ transaction: 'each' }), ); @@ -58,25 +136,78 @@ export async function runMigrations(userConfig: Partial): Promise process.exitCode = 1; } } finally { - await checkMigrationStatus(connection); + await checkMigrationStatus(connection, report); await connection.close(); resetConfig(); } return migrationsRan; } -async function checkMigrationStatus(connection: Connection) { +async function checkMigrationStatus(connection: Connection, report: (d: MigrationDiagnostic) => void) { const builderLog = await connection.driver.createSchemaBuilder().log(); if (builderLog.upQueries.length) { - log( - pc.yellow( + report({ type: 'schema-out-of-sync', queries: builderLog.upQueries.map(q => q.query) }); + } +} + +/** + * @description + * Renders a {@link MigrationDiagnostic} as lines of human-readable text. + * + * @docsCategory migration + * @since 3.7.2 + */ +export function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] { + switch (diagnostic.type) { + case 'no-migrations-matched': + return [ + 'No migration files matched the configured `migrations` patterns, but this database has migrations recorded as applied.', + `Patterns are resolved relative to the current directory (${diagnostic.cwd}):`, + ...diagnostic.patterns.map(pattern => ' - ' + pattern), + ]; + case 'schema-out-of-sync': + return [ '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)); - } + ...diagnostic.queries.map(query => ' - ' + query), + ]; + } +} + +/** + * 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 { + 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; + } + return { type: 'no-migrations-matched', patterns, cwd: 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'); } /** From abf5543d19e1d8033c2ee574c23b7b59c30917d3 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:10:29 +0000 Subject: [PATCH 03/70] docs: Generate docs for source changes --- .../reference/graphql-api/shop/mutations.mdx | 19 +++++++++- .../auth/native-authentication-strategy.mdx | 2 +- .../migration/describe-diagnostic.mdx | 17 +++++++++ .../migration/generate-migration.mdx | 2 +- .../migration/migration-diagnostic.mdx | 12 ++++++ .../no-migrations-matched-diagnostic.mdx | 38 +++++++++++++++++++ .../migration/revert-last-migration.mdx | 2 +- .../migration/run-migrations-options.mdx | 26 +++++++++++++ .../migration/run-migrations.mdx | 8 +++- .../schema-out-of-sync-diagnostic.mdx | 31 +++++++++++++++ .../request/request-context.mdx | 24 ++++++++---- .../services/channel-service.mdx | 10 +++-- .../services/customer-service.mdx | 7 ++++ .../services/product-variant-service.mdx | 8 +++- .../typescript-api/services/user-service.mdx | 25 ++++++++++++ 15 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx create mode 100644 docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx create mode 100644 docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx create mode 100644 docs/docs/reference/typescript-api/migration/run-migrations-options.mdx create mode 100644 docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx diff --git a/docs/docs/reference/graphql-api/shop/mutations.mdx b/docs/docs/reference/graphql-api/shop/mutations.mdx index e8e1244a4f0..b535bec4b2e 100644 --- a/docs/docs/reference/graphql-api/shop/mutations.mdx +++ b/docs/docs/reference/graphql-api/shop/mutations.mdx @@ -224,7 +224,7 @@ type Mutation { }} > {`""" -Register a Customer account with the given credentials. There are three possible registration flows: +Register a Customer account with the given credentials. There are four possible registration flows: _If \`authOptions.requireVerification\` is set to \`true\`:_ @@ -238,6 +238,19 @@ _If \`authOptions.requireVerification\` is set to \`true\`:_ _If \`authOptions.requireVerification\` is set to \`false\`:_ 3. The Customer _must_ be registered _with_ a password. No further action is needed - the Customer is able to authenticate immediately. + +_Whatever the setting, if an account already exists for the email address through another authentication strategy +(for example an SSO provider) and has no password yet:_ + +4. **The supplied password is never stored.** A verificationToken is created and emailed to the address, and this mutation + answers with a generic success so that it does not reveal whether the account exists. The password is set only when that + token is passed to the \`verifyCustomerAccount\` mutation _with_ the chosen password, which proves the caller controls the + mailbox. This holds even when \`requireVerification\` is \`false\`, so the Customer cannot be authenticated straight after + registering. Registering again issues a fresh token and sends the email again. + +In every flow the caller-supplied \`firstName\`, \`lastName\`, \`phoneNumber\` and custom fields are ignored whenever a User already +exists for the email address, since the caller has not proven they own it. This includes an account an administrator created +earlier. A Customer with no User, such as one left by a guest checkout, is not an account and its details are still filled in. """ type Mutation { registerCustomerAccount(input: RegisterCustomerInput!): RegisterCustomerAccountResult! @@ -609,7 +622,9 @@ type Mutation { }} > {`""" -Verify a Customer email address with the token sent to that address. Only applicable if \`authOptions.requireVerification\` is set to true. +Verify a Customer email address with the token sent to that address. Applicable whenever a verificationToken was issued: +that is when \`authOptions.requireVerification\` is set to true, and also when a password was registered against an account +that already existed through another authentication strategy, whatever that setting is. If the Customer was not registered with a password in the \`registerCustomerAccount\` mutation, the password _must_ be provided here. diff --git a/docs/docs/reference/typescript-api/auth/native-authentication-strategy.mdx b/docs/docs/reference/typescript-api/auth/native-authentication-strategy.mdx index f7ec0774995..605bfa81417 100644 --- a/docs/docs/reference/typescript-api/auth/native-authentication-strategy.mdx +++ b/docs/docs/reference/typescript-api/auth/native-authentication-strategy.mdx @@ -2,7 +2,7 @@ title: "NativeAuthenticationStrategy" generated: true --- - + This strategy implements a username/password credential-based authentication, with the credentials being stored in the Vendure database. This is the default method of authentication, and it is advised diff --git a/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx new file mode 100644 index 00000000000..4d93980f6a9 --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx @@ -0,0 +1,17 @@ +--- +title: "DescribeDiagnostic" +generated: true +--- + + +Renders a [MigrationDiagnostic](/reference/typescript-api/migration/migration-diagnostic#migrationdiagnostic) as lines of human-readable text. + +```ts title="Signature" +function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] +``` +Parameters + +### diagnostic + +MigrationDiagnostic`} /> + diff --git a/docs/docs/reference/typescript-api/migration/generate-migration.mdx b/docs/docs/reference/typescript-api/migration/generate-migration.mdx index e4872db590c..20d339d4cf8 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..0480a5d4f3f --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx @@ -0,0 +1,12 @@ +--- +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" +type MigrationDiagnostic = NoMigrationsMatchedDiagnostic | SchemaOutOfSyncDiagnostic +``` diff --git a/docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx new file mode 100644 index 00000000000..01e76c16972 --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx @@ -0,0 +1,38 @@ +--- +title: "NoMigrationsMatchedDiagnostic" +generated: true +--- + + +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. + +```ts title="Signature" +interface NoMigrationsMatchedDiagnostic { + type: 'no-migrations-matched'; + patterns: string[]; + cwd: string; +} +``` + +
+ +### type + + + + +### patterns + + + +The glob patterns configured in `dbConnectionOptions.migrations`. +### cwd + + + +The directory the patterns were resolved against. + + +
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..e8136cb2a87 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..86356fd302b --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx @@ -0,0 +1,26 @@ +--- +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 +printed to the console by default, but that output is suppressed when running from the +Vendure CLI so that the CLI can render them itself. + + +
diff --git a/docs/docs/reference/typescript-api/migration/run-migrations.mdx b/docs/docs/reference/typescript-api/migration/run-migrations.mdx index 675b7369356..27f233d70fc 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/migration/schema-out-of-sync-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx new file mode 100644 index 00000000000..c23701f98bf --- /dev/null +++ b/docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx @@ -0,0 +1,31 @@ +--- +title: "SchemaOutOfSyncDiagnostic" +generated: true +--- + + +The database schema does not match the current entity configuration, so a new migration +needs to be generated. + +```ts title="Signature" +interface SchemaOutOfSyncDiagnostic { + type: 'schema-out-of-sync'; + queries: string[]; +} +``` + +
+ +### type + + + + +### queries + + + +The SQL statements which would bring the schema back into line with the configuration. + + +
diff --git a/docs/docs/reference/typescript-api/request/request-context.mdx b/docs/docs/reference/typescript-api/request/request-context.mdx index 238af28f553..97a1982521c 100644 --- a/docs/docs/reference/typescript-api/request/request-context.mdx +++ b/docs/docs/reference/typescript-api/request/request-context.mdx @@ -42,8 +42,8 @@ myMutation(@Ctx() ctx: RequestContext) { class RequestContext { empty() => RequestContext; deserialize(ctxObject: SerializedRequestContext) => RequestContext; - userHasPermissions(permissions: Permission[]) => boolean; - userHasAllPermissions(permissions: Permission[]) => boolean; + userHasPermissions(permissions: Permission[], channelId: ID = this.channelId) => boolean; + userHasAllPermissions(permissions: Permission[], channelId: ID = this.channelId) => boolean; serialize() => SerializedRequestContext; copy(channel?: Channel) => RequestContext; req: Request | undefined @@ -81,10 +81,11 @@ Creates a new RequestContext object from a serialized object created by the `serialize()` method. ### userHasPermissions -Permission[]) => boolean`} /> +Permission[], channelId: ID = this.channelId) => boolean`} /> Returns `true` if there is an active Session & User associated with this request, -and that User has **at least one** of the specified permissions on the active Channel. +and that User has **at least one** of the specified permissions on the given Channel. +The Channel defaults to the active Channel of this request. This method uses OR logic - it checks if the user has ANY of the given permissions, not ALL of them. For AND logic, use `userHasAllPermissions`. @@ -92,15 +93,19 @@ not ALL of them. For AND logic, use `userHasAllPermissions`. *Example* ```ts -// Returns true if user has ReadProduct OR ReadCatalog +// Returns true if user has ReadProduct OR ReadCatalog on the active Channel ctx.userHasPermissions([Permission.ReadProduct, Permission.ReadCatalog]); + +// Returns true if user has UpdateChannel on the Channel with the given id +ctx.userHasPermissions([Permission.UpdateChannel], channelId); ``` ### userHasAllPermissions -Permission[]) => boolean`} since="3.6.0" /> +Permission[], channelId: ID = this.channelId) => boolean`} since="3.6.0" /> Returns `true` if there is an active Session & User associated with this request, -and that User has **all** of the specified permissions on the active Channel. +and that User has **all** of the specified permissions on the given Channel. +The Channel defaults to the active Channel of this request. This method uses AND logic - it checks if the user has EVERY one of the given permissions. For OR logic (any permission), use `userHasPermissions`. @@ -108,8 +113,11 @@ For OR logic (any permission), use `userHasPermissions`. *Example* ```ts -// Returns true only if user has BOTH ReadProduct AND UpdateProduct +// Returns true only if user has BOTH ReadProduct AND UpdateProduct on the active Channel ctx.userHasAllPermissions([Permission.ReadProduct, Permission.UpdateProduct]); + +// Returns true only if user has BOTH permissions on the Channel with the given id +ctx.userHasAllPermissions([Permission.ReadProduct, Permission.UpdateProduct], channelId); ``` ### serialize diff --git a/docs/docs/reference/typescript-api/services/channel-service.mdx b/docs/docs/reference/typescript-api/services/channel-service.mdx index cc30c7492d1..605bba5ea2d 100644 --- a/docs/docs/reference/typescript-api/services/channel-service.mdx +++ b/docs/docs/reference/typescript-api/services/channel-service.mdx @@ -2,7 +2,7 @@ title: "ChannelService" generated: true --- - + Contains methods relating to [Channel](/reference/typescript-api/entities/channel#channel) entities. @@ -84,12 +84,16 @@ Returns the default Channel. RequestContext, input: UpdateChannelInput) => Promise<ErrorResultUnionChannel>>`} /> - +Updates a Channel. Throws a ForbiddenError if the active user does not hold the +`UpdateChannel` permission on the target Channel. A SuperAdmin is exempt. A RequestContext +with no session skips the check. ### delete RequestContext, id: ID) => Promise`} /> - +Deletes a Channel. Throws a ForbiddenError if the active user does not hold the +`DeleteChannel` permission on the target Channel. A SuperAdmin is exempt. A RequestContext +with no session skips the check. ### isChannelAware VendureEntity) => entity is VendureEntity & ChannelAware`} /> diff --git a/docs/docs/reference/typescript-api/services/customer-service.mdx b/docs/docs/reference/typescript-api/services/customer-service.mdx index bd66e2b4468..1a2eefecafc 100644 --- a/docs/docs/reference/typescript-api/services/customer-service.mdx +++ b/docs/docs/reference/typescript-api/services/customer-service.mdx @@ -103,6 +103,13 @@ Registers a new Customer account with the [NativeAuthenticationStrategy](/refere the email verification flow (unless [AuthOptions](/reference/typescript-api/auth/auth-options#authoptions) `requireVerification` is set to `false`) by publishing an [AccountRegistrationEvent](/reference/typescript-api/events/event-types#accountregistrationevent). +If an account with the given email address already exists via another authentication method +(for example an external/SSO strategy) and has no native credential yet, a native credential is +added in an unactivated state (no password) and an [AccountRegistrationEvent](/reference/typescript-api/events/event-types#accountregistrationevent) is always +published, even when `requireVerification` is `false`. The password can then only be set by +whoever controls the email address, by completing the verification flow. This prevents an +unauthenticated caller from taking over such an account by registering a password against it. + This method is intended to be used in storefront Customer-creation flows. ### refreshVerificationToken diff --git a/docs/docs/reference/typescript-api/services/product-variant-service.mdx b/docs/docs/reference/typescript-api/services/product-variant-service.mdx index a317376ee8d..eda5a99981c 100644 --- a/docs/docs/reference/typescript-api/services/product-variant-service.mdx +++ b/docs/docs/reference/typescript-api/services/product-variant-service.mdx @@ -27,7 +27,7 @@ class ProductVariantService { update(ctx: RequestContext, input: UpdateProductVariantInput[]) => Promise>>; createOrUpdateProductVariantPrice(ctx: RequestContext, productVariantId: ID, price: number, channelId: ID, currencyCode?: CurrencyCode, customFields?: CustomFieldsObject) => Promise; deleteProductVariantPrice(ctx: RequestContext, variantId: ID, channelId: ID, currencyCode: CurrencyCode) => ; - softDelete(ctx: RequestContext, id: ID | ID[]) => Promise; + softDelete(ctx: RequestContext, id: ID | ID[], checkChannel: boolean = true) => Promise; hydratePriceFields(ctx: RequestContext, variant: ProductVariant, priceField: F) => Promise; applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext, order?: Order, throwIfNoPriceFound: = false) => Promise; assignProductVariantsToChannel(ctx: RequestContext, input: AssignProductVariantsToChannelInput) => Promise>>; @@ -136,9 +136,13 @@ If the `currencyCode` is not specified, the default currency of the Channel will ### softDelete -RequestContext, id: ID | ID[]) => Promise`} /> +RequestContext, id: ID | ID[], checkChannel: boolean = true) => Promise`} /> +Soft-deletes the ProductVariant(s) with the given id(s). +The lookup is scoped to the active Channel, so an id which does not belong to +`ctx.channelId` throws an [EntityNotFoundError](/reference/typescript-api/errors/error-types#entitynotfounderror). Previously an unknown or +out-of-channel id was silently reported as deleted. ### hydratePriceFields RequestContext, variant: ProductVariant, priceField: F) => Promise<ProductVariant[F]>`} /> diff --git a/docs/docs/reference/typescript-api/services/user-service.mdx b/docs/docs/reference/typescript-api/services/user-service.mdx index d73f74dcb9f..0d3467cb823 100644 --- a/docs/docs/reference/typescript-api/services/user-service.mdx +++ b/docs/docs/reference/typescript-api/services/user-service.mdx @@ -13,6 +13,7 @@ class UserService { getUserByEmailAddress(ctx: RequestContext, emailAddress: string, userType?: 'administrator' | 'customer') => Promise; createCustomerUser(ctx: RequestContext, identifier: string, password?: string) => Promise; addNativeAuthenticationMethod(ctx: RequestContext, user: User, identifier: string, password?: string) => Promise; + addUnactivatedNativeAuthenticationMethod(ctx: RequestContext, user: User, identifier: string, password?: string) => Promise; createAdminUser(ctx: RequestContext, identifier: string, password: string) => Promise; createApiKeyUser(ctx: RequestContext, roles: Role[], identifier: string) => Promise; softDelete(ctx: RequestContext, userId: ID) => ; @@ -57,6 +58,30 @@ Creates a new User with the special `customer` Role and using the [NativeAuthent Adds a new [NativeAuthenticationMethod](/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod) to the User. If the [AuthOptions](/reference/typescript-api/auth/auth-options#authoptions) `requireVerification` is set to `true` (as is the default), the User will be marked as unverified until the email verification flow is completed. +### addUnactivatedNativeAuthenticationMethod + +RequestContext, user: User, identifier: string, password?: string) => Promise<User | PasswordValidationError>`} /> + +Adds an unactivated [NativeAuthenticationMethod](/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod) (empty password, pending +`verificationToken`) to an existing User, without altering the User's `verified` state. + +If the User already carries a native credential with a pending `verificationToken`, a fresh one +is issued for it instead. This lets the mailbox owner restart the flow when the first message +was lost or its token has expired. + +The guarantee this method makes is that it never stores a password. A password already on the +credential is left as it is, and the `password` argument is discarded. + +This is used when a native credential is being registered against an account that already +exists via another authentication method (e.g. an external/SSO strategy). The credential is +left without a password so that it can only be activated by whoever controls the email +address, via the verification flow. Storing a caller-supplied password here would allow an +unauthenticated account takeover (GHSA-wr5h-x3x6-4h23). + +If a `password` is supplied it is validated and hashed, but the hash is deliberately NOT +stored. Doing the same work as a brand-new registration keeps this path indistinguishable from +one, both in its error result and in how long it takes, so it does not become an +account-existence oracle. ### createAdminUser RequestContext, identifier: string, password: string) => Promise<User>`} /> From e11bd1c256f140954edad564865e0cfd68b4e9e0 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:10:58 +0000 Subject: [PATCH 04/70] docs: Regenerate manifest.json --- docs/manifest.json | 50 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 212fe88f33e..f44a56c41c1 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1068,7 +1068,7 @@ "title": "NativeAuthenticationStrategy", "slug": "native-authentication-strategy", "file": "docs/reference/typescript-api/auth/native-authentication-strategy.mdx", - "lastModified": "2026-02-09T11:17:54+01:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "NoopSessionCacheStrategy", @@ -2286,11 +2286,17 @@ "slug": "migration", "file": "docs/reference/typescript-api/migration/index.mdx", "children": [ + { + "title": "DescribeDiagnostic", + "slug": "describe-diagnostic", + "file": "docs/reference/typescript-api/migration/describe-diagnostic.mdx", + "lastModified": "2026-08-25T09:10:29Z" + }, { "title": "GenerateMigration", "slug": "generate-migration", "file": "docs/reference/typescript-api/migration/generate-migration.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "MigrateAssetTranslationData", @@ -2304,23 +2310,47 @@ "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-08-25T09:10:29Z" + }, { "title": "MigrationOptions", "slug": "migration-options", "file": "docs/reference/typescript-api/migration/migration-options.mdx", "lastModified": "2026-01-28T14:49:21+01:00" }, + { + "title": "NoMigrationsMatchedDiagnostic", + "slug": "no-migrations-matched-diagnostic", + "file": "docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx", + "lastModified": "2026-08-25T09:10:29Z" + }, { "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-08-25T09:10:29Z" }, { "title": "RunMigrations", "slug": "run-migrations", "file": "docs/reference/typescript-api/migration/run-migrations.mdx", - "lastModified": "2026-01-28T14:49:21+01:00" + "lastModified": "2026-08-25T09:10:29Z" + }, + { + "title": "RunMigrationsOptions", + "slug": "run-migrations-options", + "file": "docs/reference/typescript-api/migration/run-migrations-options.mdx", + "lastModified": "2026-08-25T09:10:29Z" + }, + { + "title": "SchemaOutOfSyncDiagnostic", + "slug": "schema-out-of-sync-diagnostic", + "file": "docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx", + "lastModified": "2026-08-25T09:10:29Z" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -2766,7 +2796,7 @@ "title": "RequestContext", "slug": "request-context", "file": "docs/reference/typescript-api/request/request-context.mdx", - "lastModified": "2026-08-17T12:00:11+02:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "RequestContextService", @@ -2910,7 +2940,7 @@ "title": "ChannelService", "slug": "channel-service", "file": "docs/reference/typescript-api/services/channel-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "CollectionService", @@ -2940,7 +2970,7 @@ "title": "CustomerService", "slug": "customer-service", "file": "docs/reference/typescript-api/services/customer-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "EntitySlugService", @@ -3030,7 +3060,7 @@ "title": "ProductVariantService", "slug": "product-variant-service", "file": "docs/reference/typescript-api/services/product-variant-service.mdx", - "lastModified": "2026-06-24T18:05:35+02:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "PromotionService", @@ -3126,7 +3156,7 @@ "title": "UserService", "slug": "user-service", "file": "docs/reference/typescript-api/services/user-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "ZoneService", @@ -3798,7 +3828,7 @@ "title": "Mutations", "slug": "mutations", "file": "docs/reference/graphql-api/shop/mutations.mdx", - "lastModified": "2026-03-05T15:08:55+01:00" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "Queries", From c6499940c847cc71e1befb47172b3dc691a38aa8 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:28:07 +0000 Subject: [PATCH 05/70] docs: Generate docs for source changes --- .../services/administrator-service.mdx | 3 +++ .../services/fulfillment-service.mdx | 11 ++++++++++- .../typescript-api/services/history-service.mdx | 16 +++++++++------- .../typescript-api/services/order-service.mdx | 2 +- .../typescript-api/services/payment-service.mdx | 8 ++++++-- .../typescript-api/services/role-service.mdx | 12 ++++++++++++ 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/docs/reference/typescript-api/services/administrator-service.mdx b/docs/docs/reference/typescript-api/services/administrator-service.mdx index 6201b35857b..5139edb7f74 100644 --- a/docs/docs/reference/typescript-api/services/administrator-service.mdx +++ b/docs/docs/reference/typescript-api/services/administrator-service.mdx @@ -32,6 +32,9 @@ Get a paginated list of Administrators. RequestContext, administratorId: ID, relations?: RelationPaths<Administrator>) => Promise<Administrator | undefined>`} /> Get an Administrator by id. + +Resolves to `undefined` if the Administrator is not visible to the active user, so that a +caller cannot confirm the existence of an Administrator they are not allowed to see. ### findOneByUserId RequestContext, userId: ID, relations?: RelationPaths<Administrator>) => Promise<Administrator | undefined>`} /> diff --git a/docs/docs/reference/typescript-api/services/fulfillment-service.mdx b/docs/docs/reference/typescript-api/services/fulfillment-service.mdx index 9fe6bd2f398..36128c38f91 100644 --- a/docs/docs/reference/typescript-api/services/fulfillment-service.mdx +++ b/docs/docs/reference/typescript-api/services/fulfillment-service.mdx @@ -2,7 +2,7 @@ title: "FulfillmentService" generated: true --- - + Contains methods relating to [Fulfillment](/reference/typescript-api/entities/fulfillment#fulfillment) entities. @@ -37,7 +37,16 @@ Creates a new Fulfillment for the given Orders and OrderItems, using the specifi RequestContext, id: ID) => Promise<FulfillmentLine[]>`} /> +Loads the FulfillmentLines of a Fulfillment by id with no Channel check. Fulfillment is not +ChannelAware, so its only Channel boundary is the parent Order, and a caller must load that +Order in the current Channel first. +This is safe for the core callers, the `Fulfillment.lines` and `Fulfillment.summary` field +resolvers, because neither takes an id from the client: a Fulfillment is reachable only +through `Order.fulfillments` or `FulfillmentLine.fulfillment`, and neither API has a root +Fulfillment query. Scoping the read was considered and rejected, because it adds a joined +Order query per Fulfillment per request on both APIs to guard a path no client can reach. +Add the check here if a root Fulfillment query is introduced. ### getFulfillmentsLinesForOrderLine RequestContext, orderLineId: ID, relations: RelationPaths<FulfillmentLine> = []) => Promise<FulfillmentLine[]>`} /> diff --git a/docs/docs/reference/typescript-api/services/history-service.mdx b/docs/docs/reference/typescript-api/services/history-service.mdx index 2eb2b48fee8..b723d8ede59 100644 --- a/docs/docs/reference/typescript-api/services/history-service.mdx +++ b/docs/docs/reference/typescript-api/services/history-service.mdx @@ -2,7 +2,7 @@ title: "HistoryService" generated: true --- - + Contains methods relating to [HistoryEntry](/reference/typescript-api/entities/history-entry#historyentry) entities. Histories are timelines of actions related to a particular Customer or Order, recording significant events such as creation, state changes, @@ -109,9 +109,9 @@ class HistoryService { getHistoryForCustomer(ctx: RequestContext, customerId: ID, publicOnly: boolean, options?: HistoryEntryListOptions) => Promise>; createHistoryEntryForCustomer(args: CreateCustomerHistoryEntryArgs, isPublic: = false) => Promise; updateOrderHistoryEntry(ctx: RequestContext, args: UpdateOrderHistoryEntryArgs) => ; - deleteOrderHistoryEntry(ctx: RequestContext, id: ID) => Promise; + deleteOrderHistoryEntry(ctx: RequestContext, id: ID, options?: { type?: HistoryEntryType }) => Promise; updateCustomerHistoryEntry(ctx: RequestContext, args: UpdateCustomerHistoryEntryArgs) => ; - deleteCustomerHistoryEntry(ctx: RequestContext, id: ID) => Promise; + deleteCustomerHistoryEntry(ctx: RequestContext, id: ID, options?: { type?: HistoryEntryType }) => Promise; } ``` @@ -144,9 +144,10 @@ class HistoryService { ### deleteOrderHistoryEntry -RequestContext, id: ID) => Promise`} /> - +RequestContext, id: ID, options?: { type?: HistoryEntryType }) => Promise`} /> +Deletes an OrderHistoryEntry. Pass `options.type` to restrict the deletion to entries of that +type, which is how the `deleteOrderNote` mutation avoids deleting any other kind of entry. ### updateCustomerHistoryEntry RequestContext, args: UpdateCustomerHistoryEntryArgs) => `} /> @@ -154,9 +155,10 @@ class HistoryService { ### deleteCustomerHistoryEntry -RequestContext, id: ID) => Promise`} /> - +RequestContext, id: ID, options?: { type?: HistoryEntryType }) => Promise`} /> +Deletes a CustomerHistoryEntry. Pass `options.type` to restrict the deletion to entries of that +type, which is how the `deleteCustomerNote` mutation avoids deleting any other kind of entry. diff --git a/docs/docs/reference/typescript-api/services/order-service.mdx b/docs/docs/reference/typescript-api/services/order-service.mdx index 343fbfc5911..c385d15c06d 100644 --- a/docs/docs/reference/typescript-api/services/order-service.mdx +++ b/docs/docs/reference/typescript-api/services/order-service.mdx @@ -2,7 +2,7 @@ title: "OrderService" generated: true --- - + Contains methods relating to [Order](/reference/typescript-api/entities/order#order) entities. diff --git a/docs/docs/reference/typescript-api/services/payment-service.mdx b/docs/docs/reference/typescript-api/services/payment-service.mdx index b6b5f0e8a80..a2c4ae33014 100644 --- a/docs/docs/reference/typescript-api/services/payment-service.mdx +++ b/docs/docs/reference/typescript-api/services/payment-service.mdx @@ -2,7 +2,7 @@ title: "PaymentService" generated: true --- - + Contains methods relating to [Payment](/reference/typescript-api/entities/payment#payment) entities. @@ -32,7 +32,11 @@ class PaymentService { RequestContext, id: ID, relations: string[] = ['order']) => Promise<Payment>`} /> - +Loads a Payment by id with no Channel check. Payment is not ChannelAware, so callers must first +load the parent Order in the current Channel (or use a path which does, such as the private +`getPaymentInChannelOrThrow` used by the payment mutations). The only core caller is the +`Refund.lines` field resolver, which is reached from an Order already loaded in the current +Channel. ### transitionToState RequestContext, paymentId: ID, state: PaymentState) => Promise<Payment | PaymentStateTransitionError>`} /> diff --git a/docs/docs/reference/typescript-api/services/role-service.mdx b/docs/docs/reference/typescript-api/services/role-service.mdx index 4b1c4ca4096..46fe1aca179 100644 --- a/docs/docs/reference/typescript-api/services/role-service.mdx +++ b/docs/docs/reference/typescript-api/services/role-service.mdx @@ -12,6 +12,7 @@ class RoleService { initRoles() => ; findAll(ctx: RequestContext, options?: ListQueryOptions, relations?: RelationPaths) => Promise>; findOne(ctx: RequestContext, roleId: ID, relations?: RelationPaths) => Promise; + activeUserHasPermissionsOfRoles(ctx: RequestContext, roleIds: ID[]) => Promise; getChannelsForRole(ctx: RequestContext, roleId: ID) => Promise; getSuperAdminRole(ctx?: RequestContext) => Promise; getCustomerRole(ctx?: RequestContext) => Promise; @@ -43,6 +44,17 @@ class RoleService { RequestContext, roleId: ID, relations?: RelationPaths<Role>) => Promise<Role | undefined>`} /> +### activeUserHasPermissionsOfRoles + +RequestContext, roleIds: ID[]) => Promise`} since="3.7.3" /> + +Returns `true` if the active user holds every Permission granted by the given Roles, on every +Channel to which those Roles are assigned. This is the one rule which decides whether the active +user may read a Role, grant it to an Administrator, or see an Administrator who holds it. + +The Roles are always read from the database, never from the Role cache. The answer authorises +writes, and a Role missing from a stale cache would be treated as granting nothing, which is +the wrong direction to fail in. ### getChannelsForRole RequestContext, roleId: ID) => Promise<Channel[]>`} /> From a4c10430732ee1ca56423aa8d2384582ea0ba761 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:28:40 +0000 Subject: [PATCH 06/70] docs: Regenerate manifest.json --- docs/manifest.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index f44a56c41c1..e7b1d91a3c8 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2922,7 +2922,7 @@ "title": "AdministratorService", "slug": "administrator-service", "file": "docs/reference/typescript-api/services/administrator-service.mdx", - "lastModified": "2026-07-24T11:20:03+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "AssetService", @@ -2994,7 +2994,7 @@ "title": "FulfillmentService", "slug": "fulfillment-service", "file": "docs/reference/typescript-api/services/fulfillment-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "GlobalSettingsService", @@ -3006,7 +3006,7 @@ "title": "HistoryService", "slug": "history-service", "file": "docs/reference/typescript-api/services/history-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "InitializerService", @@ -3018,7 +3018,7 @@ "title": "OrderService", "slug": "order-service", "file": "docs/reference/typescript-api/services/order-service.mdx", - "lastModified": "2026-08-17T12:00:11+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "OrderTestingService", @@ -3036,7 +3036,7 @@ "title": "PaymentService", "slug": "payment-service", "file": "docs/reference/typescript-api/services/payment-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "ProductOptionGroupService", @@ -3078,7 +3078,7 @@ "title": "RoleService", "slug": "role-service", "file": "docs/reference/typescript-api/services/role-service.mdx", - "lastModified": "2026-04-20T16:08:16+02:00" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "SearchService", From 1d9804a3b0b234cdd32224ca4768bea71e300c39 Mon Sep 17 00:00:00 2001 From: Jason Millis Date: Tue, 25 Aug 2026 10:33:28 -0400 Subject: [PATCH 07/70] fix(core): validate custom fields passed to registerCustomerAccount `addRegisterCustomerCustomFieldsInput()` adds a `customFields` field to `RegisterCustomerInput` whenever the Customer entity has public, writable custom fields, so those fields can be set during registration. `CustomFieldProcessingInterceptor` never processed them. It builds its set of recognised inputs as `CreateInput` / `UpdateInput`, and `RegisterCustomerInput` matches neither, so a `validate` function on such a field runs on `updateCustomer` but is silently skipped on `registerCustomerAccount`. A rejected value produced an account that was created successfully and carried an invalid custom field, with nothing reported. Adds a small map of inputs that carry custom fields without following that naming convention, currently just `RegisterCustomerInput` -> `Customer`, and routes it through the same validation and default-application path. Registration creates a Customer, so it is treated as a create input and defaults apply as they do elsewhere. `validateInput()` also had its own copy of the entity-name derivation as an inline regex; it now calls `getEntityNameFromInputType()` so there is one implementation. The two are equivalent for every existing input (`CreateProductInput` -> `Product`, `OrderLineCustomFieldsInput` unchanged). The existing `OrderLineCustomFieldsInput` special case is deliberately left as it is, to keep the diff additive. Note this is a behaviour change: a `validate` function that previously only ran on update will now also reject invalid values at registration. That is the intent of the fix, but it may surface validators that were never exercised on this path before. --- packages/core/e2e/custom-fields.e2e-spec.ts | 50 +++++++++++++++++++ .../custom-field-processing-interceptor.ts | 31 +++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/core/e2e/custom-fields.e2e-spec.ts b/packages/core/e2e/custom-fields.e2e-spec.ts index bddace0ee2d..64a52a96a05 100644 --- a/packages/core/e2e/custom-fields.e2e-spec.ts +++ b/packages/core/e2e/custom-fields.e2e-spec.ts @@ -201,6 +201,18 @@ const customConfig = mergeConfig(testConfig(), { type: 'int', readonly: true, }, + { + // Public and writable, so it is added to RegisterCustomerInput by + // addRegisterCustomerCustomFieldsInput(). + name: 'validateOnRegister', + type: 'string', + nullable: true, + validate: value => { + if (typeof value === 'string' && value !== 'valid') { + return `The value ['${value}'] is not valid`; + } + }, + }, ], Collection: [ { name: 'secretKey1', type: 'string', defaultValue: '', public: false, internal: true }, @@ -503,6 +515,30 @@ describe('Custom fields', () => { }, 'The custom field "score" is readonly'), ); + describe('validation of custom fields on RegisterCustomerInput', () => { + it( + 'throws if a custom field value is invalid', + assertThrowsWithMessage(async () => { + await shopClient.query(registerWithCustomFieldsDocument, { + emailAddress: 'register-validation-invalid@test.com', + value: 'nope', + }); + }, "The value ['nope'] is not valid"), + ); + + it('succeeds if the custom field value is valid', async () => { + const { registerCustomerAccount } = await shopClient.query( + registerWithCustomFieldsDocument, + { + emailAddress: 'register-validation-valid@test.com', + value: 'valid', + }, + ); + + expect(registerCustomerAccount).toEqual({ success: true }); + }); + }); + it( 'throws on attempt to create readonly field', assertThrowsWithMessage(async () => { @@ -1557,6 +1593,20 @@ const getProductsFilterByIntListDocument = graphql(` } `); +const registerWithCustomFieldsDocument = graphqlShop(` + mutation RegisterWithCustomFields($emailAddress: String!, $value: String!) { + registerCustomerAccount(input: { emailAddress: $emailAddress, customFields: { validateOnRegister: $value } }) { + ... on Success { + success + } + ... on ErrorResult { + errorCode + message + } + } + } +`); + const addItemToOrderWithCustomFieldsDocument = graphqlShop(` mutation AddItemToOrderWithCustomFields($productVariantId: ID!, $quantity: Int!, $validateInt: Int!) { addItemToOrder(productVariantId: $productVariantId, quantity: $quantity, customFields: { validateInt: $validateInt }) { 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..cf26805742f 100644 --- a/packages/core/src/api/middleware/custom-field-processing-interceptor.ts +++ b/packages/core/src/api/middleware/custom-field-processing-interceptor.ts @@ -33,6 +33,20 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { private readonly createInputsWithCustomFields = new Set(); private readonly updateInputsWithCustomFields = new Set(); + /** + * Inputs which carry custom fields but do not follow the + * `CreateInput` / `UpdateInput` naming convention, mapped to the + * entity whose custom field config governs them. + * + * `RegisterCustomerInput` gains a `customFields` field from + * `addRegisterCustomerCustomFieldsInput()` whenever the Customer entity has + * public, writable custom fields - so without this entry those fields would be + * settable at registration but never validated or defaulted. + */ + private static readonly nonStandardInputEntityNames: { [inputName: string]: string } = { + RegisterCustomerInput: 'Customer', + }; + constructor( private readonly configService: ConfigService, private readonly moduleRef: ModuleRef, @@ -83,10 +97,16 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { return ( this.createInputsWithCustomFields.has(typeName) || this.updateInputsWithCustomFields.has(typeName) || + this.isNonStandardInputWithCustomFields(typeName) || typeName === 'OrderLineCustomFieldsInput' ); } + private isNonStandardInputWithCustomFields(typeName: string): boolean { + const entityName = CustomFieldProcessingInterceptor.nonStandardInputEntityNames[typeName]; + return !!entityName && !!this.configService.customFields[entityName as keyof CustomFields]; + } + private async processInputVariables( typeName: string, variableInput: any, @@ -111,6 +131,11 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { return true; } + // RegisterCustomerInput creates a Customer, so it behaves as a create input. + if (this.isNonStandardInputWithCustomFields(typeName)) { + return true; + } + // For OrderLineCustomFieldsInput, check the actual mutation name if (typeName === 'OrderLineCustomFieldsInput') { return this.isOrderLineCreateOperation(operation); @@ -217,6 +242,10 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { } private getEntityNameFromInputType(typeName: string): string { + const nonStandard = CustomFieldProcessingInterceptor.nonStandardInputEntityNames[typeName]; + if (nonStandard) { + return nonStandard; + } // Remove "Create" or "Update" prefix and "Input" suffix // e.g., "CreateProductInput" -> "Product", "UpdateCustomerInput" -> "Customer" if (typeName.startsWith('Create')) { @@ -235,7 +264,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') { From 586a941ce062d624b1a33f7f5af2633edc4738b9 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:54:33 +0000 Subject: [PATCH 08/70] docs: Regenerate manifest.json --- docs/manifest.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 2ecbe9162f6..e7b1d91a3c8 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1068,7 +1068,7 @@ "title": "NativeAuthenticationStrategy", "slug": "native-authentication-strategy", "file": "docs/reference/typescript-api/auth/native-authentication-strategy.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "NoopSessionCacheStrategy", @@ -2796,7 +2796,7 @@ "title": "RequestContext", "slug": "request-context", "file": "docs/reference/typescript-api/request/request-context.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "RequestContextService", @@ -2922,7 +2922,7 @@ "title": "AdministratorService", "slug": "administrator-service", "file": "docs/reference/typescript-api/services/administrator-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "AssetService", @@ -2940,7 +2940,7 @@ "title": "ChannelService", "slug": "channel-service", "file": "docs/reference/typescript-api/services/channel-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "CollectionService", @@ -2970,7 +2970,7 @@ "title": "CustomerService", "slug": "customer-service", "file": "docs/reference/typescript-api/services/customer-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "EntitySlugService", @@ -2994,7 +2994,7 @@ "title": "FulfillmentService", "slug": "fulfillment-service", "file": "docs/reference/typescript-api/services/fulfillment-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "GlobalSettingsService", @@ -3006,7 +3006,7 @@ "title": "HistoryService", "slug": "history-service", "file": "docs/reference/typescript-api/services/history-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "InitializerService", @@ -3018,7 +3018,7 @@ "title": "OrderService", "slug": "order-service", "file": "docs/reference/typescript-api/services/order-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "OrderTestingService", @@ -3036,7 +3036,7 @@ "title": "PaymentService", "slug": "payment-service", "file": "docs/reference/typescript-api/services/payment-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "ProductOptionGroupService", @@ -3060,7 +3060,7 @@ "title": "ProductVariantService", "slug": "product-variant-service", "file": "docs/reference/typescript-api/services/product-variant-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "PromotionService", @@ -3078,7 +3078,7 @@ "title": "RoleService", "slug": "role-service", "file": "docs/reference/typescript-api/services/role-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T12:28:07Z" }, { "title": "SearchService", @@ -3156,7 +3156,7 @@ "title": "UserService", "slug": "user-service", "file": "docs/reference/typescript-api/services/user-service.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "ZoneService", @@ -3828,7 +3828,7 @@ "title": "Mutations", "slug": "mutations", "file": "docs/reference/graphql-api/shop/mutations.mdx", - "lastModified": "2026-08-25T13:57:08Z" + "lastModified": "2026-08-25T09:10:29Z" }, { "title": "Queries", From 008e02adcf9f01fb745b2cea3e0cfdbd056aa8bc Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Tue, 1 Sep 2026 10:39:37 +0200 Subject: [PATCH 09/70] fix(core): scope the migration diagnostics to actionable cases Address review feedback on #5174: - Only report `schema-out-of-sync` when the database has migration history. Against a database with nothing applied the schema builder returns the entire schema as pending, so a first `vendure migrate --run` on a freshly created project printed every CREATE TABLE as a warning where it previously printed one success line. - Cap the rendered drift queries at 10. The queries themselves stay on the diagnostic for programmatic consumers. - Only point at the working directory when a configured pattern is relative. The scaffolded config uses `path.join(__dirname, ...)`, where the cwd does not explain the failure. - Correct the `onDiagnostic` doc comment: suppression comes from `VENDURE_RUNNING_IN_CLI`, and passing the callback does not change it. - Use the singular in the CLI summary for a single migration. Relates to #5001 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013bLmE6tqF198Z7TxHPy52e --- packages/cli/e2e/migrate-command.e2e-spec.ts | 3 ++ .../commands/migrate/migration-report.spec.ts | 22 ++++++++++- .../src/commands/migrate/migration-report.ts | 2 +- packages/core/src/migrate.ts | 37 ++++++++++++++++--- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/cli/e2e/migrate-command.e2e-spec.ts b/packages/cli/e2e/migrate-command.e2e-spec.ts index d2bcd8a45a7..17d994519fd 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 migration history is not "out of sync": every table is + // pending because nothing has been applied yet. + expect(result.hasWarnings).toBe(false); expect(result.migrationsRan).toBeDefined(); expect(result.migrationsRan).toHaveLength(0); }); diff --git a/packages/cli/src/commands/migrate/migration-report.spec.ts b/packages/cli/src/commands/migrate/migration-report.spec.ts index 48d86d05272..579b42ff060 100644 --- a/packages/cli/src/commands/migrate/migration-report.spec.ts +++ b/packages/cli/src/commands/migrate/migration-report.spec.ts @@ -43,13 +43,31 @@ describe('buildMigrationReport()', () => { it('appends the schema drift warning to the summary', () => { const report = buildMigrationReport(['1700000000000-first'], [outOfSync]); - expect(report.summary).toBe('Successfully ran 1 migrations'); - expect(report.message).toContain('Successfully ran 1 migrations'); + 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); }); + it('does not blame the working directory when the patterns are absolute', () => { + const report = buildMigrationReport([], [ + { type: 'no-migrations-matched', patterns: ['/project/dist/migrations/*.js'], cwd: '/elsewhere' }, + ]); + + expect(report.details).toContain('/project/dist/migrations/*.js'); + expect(report.details).not.toContain('/elsewhere'); + }); + + it('caps the rendered drift queries', () => { + const queries = Array.from({ length: 25 }, (_, i) => `CREATE TABLE \`t${i}\` (id int)`); + const report = buildMigrationReport([], [{ type: 'schema-out-of-sync', queries }]); + + expect(report.details).toContain('CREATE TABLE `t9` (id int)'); + expect(report.details).not.toContain('CREATE TABLE `t10` (id int)'); + expect(report.details).toContain('...and 15 more changes'); + }); + it('renders every diagnostic', () => { const report = buildMigrationReport([], [unmatched, outOfSync]); diff --git a/packages/cli/src/commands/migrate/migration-report.ts b/packages/cli/src/commands/migrate/migration-report.ts index b9861067512..7edd17d2b6b 100644 --- a/packages/cli/src/commands/migrate/migration-report.ts +++ b/packages/cli/src/commands/migrate/migration-report.ts @@ -23,7 +23,7 @@ export function buildMigrationReport( // pending, they just could not be loaded. const patternsUnmatched = diagnostics.some(d => d.type === 'no-migrations-matched'); const summary = migrationsRan.length - ? `Successfully ran ${migrationsRan.length} migrations` + ? `Successfully ran ${migrationsRan.length} migration${migrationsRan.length === 1 ? '' : 's'}` : patternsUnmatched ? 'No migration files could be loaded' : 'No pending migrations found'; diff --git a/packages/core/src/migrate.ts b/packages/core/src/migrate.ts index 2d332ee91f8..3a5fa37e682 100644 --- a/packages/core/src/migrate.ts +++ b/packages/core/src/migrate.ts @@ -91,8 +91,9 @@ export interface RunMigrationsOptions { /** * @description * Invoked for each {@link MigrationDiagnostic} detected during the run. These conditions are - * printed to the console by default, but that output is suppressed when running from the - * Vendure CLI so that the CLI can render them itself. + * 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; } @@ -144,12 +145,25 @@ export async function runMigrations( } async function checkMigrationStatus(connection: Connection, report: (d: MigrationDiagnostic) => void) { + // Against a database with no migration history the schema builder reports the entire schema + // as pending, which is what a project looks like before its first migration is applied. That + // is not drift, so there is nothing useful to say about it. + const executed = await new MigrationExecutor(connection).getExecutedMigrations(); + if (!executed.length) { + return; + } const builderLog = await connection.driver.createSchemaBuilder().log(); if (builderLog.upQueries.length) { report({ type: 'schema-out-of-sync', queries: builderLog.upQueries.map(q => q.query) }); } } +/** + * The full drift list can be the entire schema, which is more than a terminal should be asked + * to render. The queries themselves remain on the diagnostic for programmatic consumers. + */ +const maxDescribedQueries = 10; + /** * @description * Renders a {@link MigrationDiagnostic} as lines of human-readable text. @@ -159,17 +173,28 @@ async function checkMigrationStatus(connection: Connection, report: (d: Migratio */ export function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] { switch (diagnostic.type) { - case 'no-migrations-matched': + case 'no-migrations-matched': { + // 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 relative = diagnostic.patterns.some(pattern => !path.isAbsolute(pattern)); return [ 'No migration files matched the configured `migrations` patterns, but this database has migrations recorded as applied.', - `Patterns are resolved relative to the current directory (${diagnostic.cwd}):`, + relative + ? `Patterns are resolved relative to the current directory (${diagnostic.cwd}):` + : 'Nothing on disk matches these patterns. If they point at compiled output, check that it has been built:', ...diagnostic.patterns.map(pattern => ' - ' + pattern), ]; - case 'schema-out-of-sync': + } + case 'schema-out-of-sync': { + const shown = diagnostic.queries.slice(0, maxDescribedQueries); + const remaining = diagnostic.queries.length - shown.length; return [ 'Your database schema does not match your current configuration. Generate a new migration for the following changes:', - ...diagnostic.queries.map(query => ' - ' + query), + ...shown.map(query => ' - ' + query), + ...(remaining ? [` ...and ${remaining} more change${remaining === 1 ? '' : 's'}`] : []), ]; + } } } From 59259b1eede9797341cad08df6f3ed42d9222cc8 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:40:40 +0000 Subject: [PATCH 10/70] docs: Generate docs for source changes --- .../typescript-api/migration/describe-diagnostic.mdx | 2 +- .../typescript-api/migration/generate-migration.mdx | 2 +- .../typescript-api/migration/revert-last-migration.mdx | 2 +- .../typescript-api/migration/run-migrations-options.mdx | 5 +++-- .../reference/typescript-api/migration/run-migrations.mdx | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx index 4d93980f6a9..189156f6c73 100644 --- a/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx +++ b/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx @@ -2,7 +2,7 @@ title: "DescribeDiagnostic" generated: true --- - + Renders a [MigrationDiagnostic](/reference/typescript-api/migration/migration-diagnostic#migrationdiagnostic) as lines of human-readable text. diff --git a/docs/docs/reference/typescript-api/migration/generate-migration.mdx b/docs/docs/reference/typescript-api/migration/generate-migration.mdx index 20d339d4cf8..21b20e5574d 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/revert-last-migration.mdx b/docs/docs/reference/typescript-api/migration/revert-last-migration.mdx index e8136cb2a87..55a8a2f3bfa 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 index 86356fd302b..d69f021855e 100644 --- a/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx +++ b/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx @@ -19,8 +19,9 @@ interface RunMigrationsOptions { MigrationDiagnostic) => void`} /> Invoked for each [MigrationDiagnostic](/reference/typescript-api/migration/migration-diagnostic#migrationdiagnostic) detected during the run. These conditions are -printed to the console by default, but that output is suppressed when running from the -Vendure CLI so that the CLI can render them itself. +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 27f233d70fc..d40f5e07211 100644 --- a/docs/docs/reference/typescript-api/migration/run-migrations.mdx +++ b/docs/docs/reference/typescript-api/migration/run-migrations.mdx @@ -2,7 +2,7 @@ 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. From 74ebf6cf457d9d2c7418a784dc9cc26f311d8c30 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:41:09 +0000 Subject: [PATCH 11/70] docs: Regenerate manifest.json --- docs/manifest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index e7b1d91a3c8..d52bada53ea 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2290,13 +2290,13 @@ "title": "DescribeDiagnostic", "slug": "describe-diagnostic", "file": "docs/reference/typescript-api/migration/describe-diagnostic.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-09-01T08:40:40Z" }, { "title": "GenerateMigration", "slug": "generate-migration", "file": "docs/reference/typescript-api/migration/generate-migration.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-09-01T08:40:40Z" }, { "title": "MigrateAssetTranslationData", @@ -2332,19 +2332,19 @@ "title": "RevertLastMigration", "slug": "revert-last-migration", "file": "docs/reference/typescript-api/migration/revert-last-migration.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-09-01T08:40:40Z" }, { "title": "RunMigrations", "slug": "run-migrations", "file": "docs/reference/typescript-api/migration/run-migrations.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-09-01T08:40:40Z" }, { "title": "RunMigrationsOptions", "slug": "run-migrations-options", "file": "docs/reference/typescript-api/migration/run-migrations-options.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-09-01T08:40:40Z" }, { "title": "SchemaOutOfSyncDiagnostic", From a747081a7171414df9b1c56d64dd2f5cb6808092 Mon Sep 17 00:00:00 2001 From: wakqasahmed Date: Thu, 3 Sep 2026 08:54:02 +0200 Subject: [PATCH 12/70] test(core): add e2e coverage for createFromFileStream without a RequestContext Closes #5204 --- packages/core/e2e/asset.e2e-spec.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/core/e2e/asset.e2e-spec.ts b/packages/core/e2e/asset.e2e-spec.ts index 27cf229eba2..dd8bec5e94d 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 } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import fs from 'fs-extra'; import path from 'node:path'; @@ -638,4 +638,26 @@ describe('Asset resolver', () => { expect(createAssets[0]).not.toHaveProperty('message'); }); }); + + // 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(['test file content']), + 'test-file.pdf', + ); + + assetGuard.assertSuccess(result); + expect(result.name).toBe('test-file.pdf'); + expect(result.translations.map(t => t.languageCode)).toEqual([ + defaultChannel.defaultLanguageCode, + ]); + expect(result.channels.map(c => c.id)).toEqual([defaultChannel.id]); + }); + }); }); From 4cbbc790118786a17721954c3a4d739810bd2632 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 12:20:17 +0300 Subject: [PATCH 13/70] fix(core): do not record promotions that apply no discount A Promotion whose conditions pass but whose actions produce no adjustment anywhere is still attached to the Order, and PromotionService counts usageLimit / perCustomerUsageLimit from exactly those join rows. A customer who received nothing from a coupon still burns their allowed use. OrderCalculator now drops a Promotion from order.promotions at the end of applyPriceAdjustments unless it adjusted an OrderLine or a ShippingLine. Promotions whose actions define onActivate / onDeactivate are kept: their benefit 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. Coupon codes are unaffected - a code the customer typed stays on the Order even when it discounts nothing. --- .../guides/core-concepts/promotions/index.mdx | 4 + packages/core/e2e/order-promotion.e2e-spec.ts | 87 ++++++++ .../src/config/promotion/promotion-action.ts | 11 + .../src/entity/promotion/promotion.entity.ts | 12 ++ .../order-calculator/order-calculator.spec.ts | 191 ++++++++++++++++++ .../order-calculator/order-calculator.ts | 32 +++ 6 files changed, 337 insertions(+) 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/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/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/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/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, + ); + } } From b19ab4dc76e4f8521e02258b8afa26f615142595 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 12:30:06 +0300 Subject: [PATCH 14/70] fix(core): apply relevance ordering when search sort input is empty The DefaultSearchPlugin search strategies treated `input.sort` as an explicit sort request whenever it was truthy. Clients generated with graphql-codegen commonly send `sort: {}` when the user has not selected a sort order, and an empty object is truthy, so the sort branch was taken, no ORDER BY field was added from it, and the `score DESC` ordering was skipped entirely. The effect was that term searches fell back to the deterministic `productVariantId ASC` tiebreaker, so the relevance score, which is still computed and returned in the SELECT, had no influence on the result order. Exact matches could be returned below unrelated items. `hasSortParameter()` now treats a sort object with no defined fields as absent, so relevance ordering applies. Behaviour is unchanged when at least one sort field is set. All three strategies (Postgres, MySQL / MariaDB, SQLite / sql.js) shared the defect and are fixed together. --- .../e2e/default-search-plugin.e2e-spec.ts | 24 ++++++++++++++ .../search-strategy/mysql-search-strategy.ts | 3 +- .../postgres-search-strategy.ts | 3 +- .../search-strategy-utils.spec.ts | 31 +++++++++++++++++++ .../search-strategy/search-strategy-utils.ts | 13 ++++++++ .../search-strategy/sqlite-search-strategy.ts | 3 +- 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.spec.ts 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/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); From 748abd5dac13b61eb6fafd9d6bc5407a7eaa3910 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 16:34:52 +0300 Subject: [PATCH 15/70] docs: align commit message examples with the enforced commitlint rules --- CONTRIBUTING.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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. From d6e48b7388806a3473622f5e4de72df0a79abcdc Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 02:16:29 +0300 Subject: [PATCH 16/70] fix(core): Export SubscribableJob and documented entities from the barrel SubscribableJob is the return type of JobQueue.add() and is handed back by JobQueueService, but the class was not re-exported from packages/core/src/job-queue/index.ts, so plugin code had to import it from '@vendure/core/dist/job-queue/subscribable-job'. The same line also exports JobUpdate and JobUpdateOptions, which its updates() method uses. Six entities that carry an "@docsCategory entities" annotation, and so have a generated page under docs/reference/typescript-api/entities, were missing from packages/core/src/entity/index.ts: Region (region.mdx) ApiKey (api-key.mdx) HistoryEntry (history-entry.mdx) OrderHistoryEntry (order-history-entry.mdx) CustomerHistoryEntry (customer-history-entry.mdx) OrderModification (order-modification.mdx) Each was reachable only through a deep import into dist/, which pins consumers to core's internal file layout. Several are surrounded by siblings the barrel already exports: Country and Province are exported while their base class Region was not, and OrderModificationLine is exported while OrderModification was not. After this change every entity with a published reference page is exported from the barrel, with no exceptions. Undocumented entities are left alone. The five translation entities absent from the barrel (RegionTranslation, PromotionTranslation, PaymentMethodTranslation, ShippingMethodTranslation, ApiKeyTranslation) carry no @docsCategory annotation and have no reference page, and the barrel is already split on them - eight translation entities are exported and five are not, with nothing documenting the line. Settling that split is a separate call for maintainers, not drift to fix here. No symbol is moved, renamed or removed, and none of the added names collides with an existing export. --- packages/core/src/entity/index.ts | 6 ++++++ packages/core/src/job-queue/index.ts | 1 + 2 files changed, 7 insertions(+) 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/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'; From 832742d709f12c59765593d99af5d8427f910971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:07:06 +0200 Subject: [PATCH 17/70] chore(deps): bump @typescript-eslint/eslint-plugin from 5.62.0 to 8.68.0 (#5240) --- bun.lock | 156 +++++++++++++-------------------- package.json | 2 +- packages/admin-ui/package.json | 2 +- 3 files changed, 62 insertions(+), 98 deletions(-) diff --git a/bun.lock b/bun.lock index 42685f766b8..52675211867 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", @@ -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", @@ -2583,25 +2583,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.68.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.68.0", "@typescript-eslint/type-utils": "8.68.0", "@typescript-eslint/utils": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.68.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q=="], "@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.68.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.68.0", "@typescript-eslint/types": "^8.68.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw=="], - "@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.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.0" } }, "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA=="], - "@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.68.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw=="], - "@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.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "@typescript-eslint/typescript-estree": "8.68.0", "@typescript-eslint/utils": "8.68.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-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg=="], "@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.68.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/typescript-estree": "8.68.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ=="], - "@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.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A=="], "@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=="], @@ -3791,7 +3791,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=="], @@ -4433,8 +4433,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=="], @@ -5373,7 +5371,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=="], @@ -5675,22 +5673,14 @@ "@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/schematics/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@angular-eslint/schematics/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "@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=="], @@ -5813,6 +5803,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 +5813,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=="], @@ -6169,15 +6163,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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.68.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.68.0", "@typescript-eslint/tsconfig-utils": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.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-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA=="], + + "@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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.68.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.68.0", "@typescript-eslint/tsconfig-utils": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.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-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA=="], - "@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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + + "@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 +6359,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=="], @@ -6419,6 +6427,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=="], @@ -6775,8 +6785,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=="], @@ -7133,22 +7141,6 @@ "@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/build/@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "@angular/build/@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -7561,7 +7553,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 +7581,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 +7707,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=="], @@ -8295,36 +8295,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=="], @@ -8545,6 +8515,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 +8527,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=="], @@ -8597,8 +8577,6 @@ "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 +8713,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 +8811,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=="], diff --git a/package.json b/package.json index f2f05caab89..4dd26f141f2 100644 --- a/package.json +++ b/package.json @@ -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", From 1cca8e21773a154382d6ada546fe8252bb09dca0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:09:36 +0200 Subject: [PATCH 18/70] chore(repo): bump @commitlint/config-conventional from 19.8.1 to 21.2.2 (#5236) --- bun.lock | 16 +++++++++++++--- package.json | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 52675211867..3135488b97f 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -1055,7 +1055,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 +1087,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=="], @@ -2029,6 +2031,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=="], @@ -2823,6 +2827,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 +3115,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=="], @@ -5785,6 +5791,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=="], @@ -7309,6 +7317,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=="], diff --git a/package.json b/package.json index 4dd26f141f2..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", From 811be148a1989593bded83c77090f6492895f0ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:15:10 +0200 Subject: [PATCH 19/70] chore(create): bump open from 10.2.0 to 11.0.1 (#5238) --- bun.lock | 20 ++++++++++++++------ packages/create/package.json | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 3135488b97f..7e52ab17930 100644 --- a/bun.lock +++ b/bun.lock @@ -305,12 +305,12 @@ }, "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 +320,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", @@ -4551,7 +4551,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=="], @@ -4785,7 +4785,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=="], @@ -5611,7 +5611,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=="], @@ -6991,6 +6991,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=="], @@ -7091,6 +7093,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=="], @@ -8113,6 +8117,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=="], @@ -8191,6 +8197,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=="], 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" From f3a0df2a96ca04eee316bf353dbde19fbbf2d7b3 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 7 Sep 2026 11:08:41 +0200 Subject: [PATCH 20/70] ci: build against a fresh resolve of the declared ranges weekly (#5232) --- .github/workflows/floating_deps_check.yml | 114 ++++++++ .../workflows/scripts/floating-deps-report.js | 246 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 .github/workflows/floating_deps_check.yml create mode 100644 .github/workflows/scripts/floating-deps-report.js diff --git a/.github/workflows/floating_deps_check.yml b/.github/workflows/floating_deps_check.yml new file mode 100644 index 00000000000..5771c063f1e --- /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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + 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/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'], + }); +} From c878495e18543973ccb0310d055363f666fe2953 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:08:57 +0200 Subject: [PATCH 21/70] chore(deps): bump the github-actions group across 1 directory with 10 updates (#5212) --- .github/workflows/back_merge.yml | 4 ++-- .github/workflows/build_and_test.yml | 24 +++++++++---------- .github/workflows/cla.yml | 4 ++-- .github/workflows/codegen.yml | 4 ++-- .github/workflows/deploy_dashboard.yml | 2 +- .github/workflows/docs_ci.yml | 6 ++--- .github/workflows/generate_docs.yml | 8 +++---- .github/workflows/lint_pr_title.yml | 2 +- .github/workflows/publish_and_install.yml | 28 +++++++++++------------ .github/workflows/publish_to_npm.yml | 4 ++-- .github/workflows/stale_bot.yml | 2 +- 11 files changed, 44 insertions(+), 44 deletions(-) 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/generate_docs.yml b/.github/workflows/generate_docs.yml index 7c3ab21dc00..bc8c59914d1 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@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1 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/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 From d3c2e0be201a143c7bd9a716eef3006e0efc6894 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 7 Sep 2026 15:31:19 +0200 Subject: [PATCH 22/70] chore(core): fix build failures against freshly resolved dependency ranges (#5299) --- bun.lock | 114 +++++++++++++---------- packages/core/package.json | 2 + packages/core/src/plugin/plugin-utils.ts | 6 -- 3 files changed, 69 insertions(+), 53 deletions(-) diff --git a/bun.lock b/bun.lock index 7e52ab17930..9f5e26b59e7 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -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,7 +301,7 @@ }, "packages/create": { "name": "@vendure/create", - "version": "3.7.2", + "version": "3.7.3", "bin": { "create": "./index.js", }, @@ -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", @@ -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", @@ -1317,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=="], @@ -5671,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=="], @@ -5679,7 +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-template/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + "@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=="], @@ -5833,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=="], @@ -5861,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=="], @@ -5917,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=="], @@ -6445,6 +6441,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=="], @@ -7153,6 +7151,8 @@ "@angular-devkit/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@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=="], "@angular/build/@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -7335,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=="], @@ -8445,8 +8461,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=="], 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/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); }, From 7ad46788348bd978ced8db1e4dc7be7599d6248a Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:32:20 +0000 Subject: [PATCH 23/70] docs: Generate docs for source changes --- docs/docs/reference/typescript-api/plugin/plugin-utilities.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). From 7a77767152d48888644475c6394c29f7cc3c3849 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:32:54 +0000 Subject: [PATCH 24/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 1e1f2c40964..53d69c86009 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2622,7 +2622,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", From 128ba8b800137ceeeb353e59ce6bf629ac6a947c Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:12:38 +0000 Subject: [PATCH 25/70] @cuyl has signed the CLA in vendurehq/vendure#5302 --- license/signatures/version1/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/license/signatures/version1/cla.json b/license/signatures/version1/cla.json index f37bc981c75..5d3f3ab31b8 100644 --- a/license/signatures/version1/cla.json +++ b/license/signatures/version1/cla.json @@ -1383,6 +1383,14 @@ "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 } ] } \ No newline at end of file From 433c9577e7501276dc8e7018eec546bb9c8f212b Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 02:22:32 +0300 Subject: [PATCH 26/70] fix(core): log stack trace and error class for failed scheduled tasks When a ScheduledTask throws, DefaultSchedulerStrategy logged only error.message. An Error subclass thrown with an empty message therefore produced a log line that ended after the colon, with no class name, no stack and no cause to act on. Log util.inspect(error) instead. Node already renders everything needed here: the constructor name when the message is empty, the stack, extra own properties, and the whole cause chain including causes which are not Errors, such as cause: 'ECONNRESET'. Cycles are handled by inspect itself, which prints [Circular *1]. No trace argument is passed to Logger.error any more. DefaultLogger appends trace to the message, and the inspect output already contains the stack, so passing it would print the stack twice. The lastResult record is deliberately not the inspect output. It is surfaced in the Dashboard, so it keeps a short single-line label: error.message falling back to the constructor name. Test coverage moves into the existing default-scheduler-plugin e2e spec, which now runs a task throwing a subclass that sets neither a message nor a name, the shape which used to log as an empty string. Its cause is assigned after construction because Error.cause is not in the lib used here, though it is populated at runtime; a non-Error cause is the case that matters. The test asserts on TestingLogger.errorSpy. --- .../e2e/default-scheduler-plugin.e2e-spec.ts | 41 ++++++++++++++++++- .../default-scheduler-strategy.ts | 20 ++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) 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/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; +} From c3a3d560b9e425281841e33a42e8c702a74e6464 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Sat, 5 Sep 2026 02:09:21 +0300 Subject: [PATCH 27/70] fix(dashboard): Keep column filters AND-ed with the list search term The list pages that search across several fields did so by setting a page-level `filterOperator: 'OR'`. That operator joins every top-level condition in the filter object, and the column & faceted filters are merged into that same object, so entering a search term OR-ed away the active filters and the list showed rows matching neither. Express the search as an explicit `_or` group inside the filter instead, which leaves the column filters AND-ed. Affects the products, product variants, customers, administrators, promotions and countries lists, and the customer group members table. --- .../dashboard/list-views/list-page.mdx | 13 ++++-- .../e2e/tests/catalog/product-list.spec.ts | 41 +++++++++++++++++++ .../_administrators/administrators.tsx | 16 +++----- .../_authenticated/_countries/countries.tsx | 12 +----- .../customer-group-members-table.tsx | 10 ++--- .../_authenticated/_customers/customers.tsx | 16 +++----- .../_product-variants/product-variants.tsx | 11 +---- .../_authenticated/_products/products.tsx | 16 +++----- .../_authenticated/_promotions/promotions.tsx | 11 +---- .../lib/framework/page/list-page.stories.tsx | 32 +-------------- .../src/lib/framework/page/list-page.tsx | 13 ++++-- 11 files changed, 85 insertions(+), 106 deletions(-) diff --git a/docs/docs/reference/dashboard/list-views/list-page.mdx b/docs/docs/reference/dashboard/list-views/list-page.mdx index dbcf2ec88d0..6e9824c4909 100644 --- a/docs/docs/reference/dashboard/list-views/list-page.mdx +++ b/docs/docs/reference/dashboard/list-views/list-page.mdx @@ -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/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/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/_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/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 } }, + * ], * }; * }} * /> From 6a3dcd6ff97b597389ae1d98af4d14824a642ac2 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:33:51 +0000 Subject: [PATCH 28/70] @Ismatulla has signed the CLA in vendurehq/vendure#5313 --- license/signatures/version1/cla.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/license/signatures/version1/cla.json b/license/signatures/version1/cla.json index 5d3f3ab31b8..3b7fdb8e83a 100644 --- a/license/signatures/version1/cla.json +++ b/license/signatures/version1/cla.json @@ -1391,6 +1391,14 @@ "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 From cfc92529182484c8e137887390cd6e4760c29584 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:47:52 +0000 Subject: [PATCH 29/70] docs: Generate docs for source changes --- .../scheduled-tasks/default-scheduler-strategy.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a85de46d04b23c218abaa73437651d1fa3de0572 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:48:21 +0000 Subject: [PATCH 30/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 53d69c86009..6510a398efa 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2810,7 +2810,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", From cdf08be69744852d176043e1c84bb7af35e88812 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:38:12 +0000 Subject: [PATCH 31/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 6510a398efa..2940d01bc7a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -4219,7 +4219,7 @@ "title": "ListPage", "slug": "list-page", "file": "docs/reference/dashboard/list-views/list-page.mdx", - "lastModified": "2026-08-10T09:00:52Z" + "lastModified": "2026-09-07T20:52:51+03:00" }, { "title": "PaginatedListDataTable", From 35c2251069a369b4c88a651f7ac66f4343bcd691 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:38:39 +0000 Subject: [PATCH 32/70] docs: Generate docs for source changes --- docs/docs/reference/dashboard/list-views/list-page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/reference/dashboard/list-views/list-page.mdx b/docs/docs/reference/dashboard/list-views/list-page.mdx index 6e9824c4909..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. From 01b752cb4b7ec146726c54d23b799a4df65d876e Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:39:15 +0000 Subject: [PATCH 33/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 2940d01bc7a..1d564e71c7b 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -4219,7 +4219,7 @@ "title": "ListPage", "slug": "list-page", "file": "docs/reference/dashboard/list-views/list-page.mdx", - "lastModified": "2026-09-07T20:52:51+03:00" + "lastModified": "2026-09-08T10:38:40Z" }, { "title": "PaginatedListDataTable", From 2354e813ed228fc5846f980cc7841cd5987765d7 Mon Sep 17 00:00:00 2001 From: Anh Bui Quang Date: Tue, 8 Sep 2026 16:41:57 +0200 Subject: [PATCH 34/70] docs(core): document that /health no longer runs health check strategies (#5301) --- .../core-concepts/healthchecks/index.mdx | 64 ++++++------------- docs/docs/guides/deployment/using-docker.mdx | 17 ++--- .../configuration/runtime-vendure-config.mdx | 2 +- .../configuration/system-options.mdx | 7 +- .../configuration/vendure-config.mdx | 2 +- .../health-check/health-check-error.mdx | 4 +- .../health-check-registry-service.mdx | 25 ++------ .../health-check/health-check-strategy.mdx | 24 ++----- .../http-health-check-strategy.mdx | 5 +- .../type-ormhealth-check-strategy.mdx | 9 +-- .../config/system/health-check-strategy.ts | 27 ++------ packages/core/src/config/vendure-config.ts | 9 +-- .../health-check-registry.service.ts | 23 ++----- .../http-health-check-strategy.ts | 3 +- .../core/src/health-check/terminus-compat.ts | 4 +- .../typeorm-health-check-strategy.ts | 7 +- 16 files changed, 71 insertions(+), 161 deletions(-) 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/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/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/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/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/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 }), * ] * } From 4c1be1236ad5f09b000c656c82218b6ae84c8cc7 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:42:30 +0000 Subject: [PATCH 35/70] docs: Regenerate manifest.json --- docs/manifest.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 1d564e71c7b..eb03479450c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", @@ -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" @@ -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" From c0cf79c3420ae319878e7a56cb057e7a4323d74f Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:26:48 +0000 Subject: [PATCH 36/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index eb03479450c..3936b7d0e09 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", From e07f64cbe247c449ceb0aa89f750ced0fc7b0d32 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:27:17 +0000 Subject: [PATCH 37/70] docs: Generate docs for source changes --- .../typescript-api/promotions/promotion-action.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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. From 132953620a37664a254887fb800de9b497a8fdca Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:27:48 +0000 Subject: [PATCH 38/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 3936b7d0e09..572d3eb9117 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2716,7 +2716,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", From 1b7dfedb9c922e8ab88662216aa14af7ab9ac9b0 Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:28:15 +0000 Subject: [PATCH 39/70] docs: Generate docs for source changes --- .../default-search-plugin/mysql-search-strategy.mdx | 2 +- .../default-search-plugin/postgres-search-strategy.mdx | 2 +- .../default-search-plugin/sqlite-search-strategy.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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. From 9534b25bc85eafed316c028d360676c1e4eaea7a Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:28:45 +0000 Subject: [PATCH 40/70] docs: Regenerate manifest.json --- docs/manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 572d3eb9117..ca3fc40b03e 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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" From b7d9f0b6f0ef20f9b0c1bc67b02d3571a20043a6 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Tue, 8 Sep 2026 18:42:12 +0300 Subject: [PATCH 41/70] docs: lowercase the PR title example in the localization guide --- docs/docs/guides/extending-the-dashboard/localization/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From dbf74d43f7fca3fa2b540bd57ccefd671cf2aded Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:00:14 +0000 Subject: [PATCH 42/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index ca3fc40b03e..37d6ad27eb3 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", From e0bf4b0e66d2e10a6cea9ae85cf60b81a2507706 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 8 Sep 2026 19:55:19 +0200 Subject: [PATCH 43/70] chore(docs): update js-yaml to 3.15.2 to resolve security advisories (#5319) --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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", From 4a76d70ec4ed2ea4a16b3e02d78e70e48c10b863 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 9 Sep 2026 07:52:36 +0200 Subject: [PATCH 44/70] fix(cli): keep vendure dev resilient to a compile-error crash (#5323) --- packages/cli/src/commands/dev/dev.spec.ts | 170 +++++++++++++++++++++- packages/cli/src/commands/dev/dev.ts | 158 ++++++++++++++++---- 2 files changed, 290 insertions(+), 38 deletions(-) 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()) { From beb505b9cc0d899233e414e662275044cffe1132 Mon Sep 17 00:00:00 2001 From: StefanOrzu Date: Wed, 9 Sep 2026 13:26:34 +0100 Subject: [PATCH 45/70] fix(dashboard): include imported JSON in dashboard config compilation (#4808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: Kevin Co-authored-by: Drayke Co-authored-by: Michael Bromley Co-authored-by: thestreamcode Co-authored-by: wakqasahmed Co-authored-by: Deepak Maharana <100968930+deep231w@users.noreply.github.com> Co-authored-by: Housein Abo Shaar <76689341+HouseinIsProgramming@users.noreply.github.com> Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com> Co-authored-by: ryrahul Co-authored-by: Will Nahmens <63622091+biggamesmallworld@users.noreply.github.com> Co-authored-by: Will Nahmens Co-authored-by: Claude Co-authored-by: vendure-ci-automation-bot[bot] <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Co-authored-by: Michael Bromley Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Höck Co-authored-by: Ihor Barmak Co-authored-by: Anh Bui Quang Co-authored-by: Michael Bromley --- .../fixtures-json-pkg/my-plugin/package.json | 5 ++ .../my-plugin/src/my.plugin.ts | 12 +++ .../vite/tests/fixtures-json-pkg/package.json | 6 ++ .../tests/fixtures-json-pkg/vendure-config.ts | 11 +++ .../vite/tests/fixtures-json/config-data.json | 1 + .../fixtures-json/my-plugin/src/my.plugin.ts | 12 +++ .../my-plugin/src/plugin-data.json | 1 + .../vite/tests/fixtures-json/package.json | 6 ++ .../tests/fixtures-json/vendure-config.ts | 13 ++++ .../dashboard/vite/tests/json-imports.spec.ts | 78 +++++++++++++++++++ packages/dashboard/vite/utils/compiler.ts | 54 ++++++++++--- 11 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/package.json create mode 100644 packages/dashboard/vite/tests/fixtures-json-pkg/my-plugin/src/my.plugin.ts create mode 100644 packages/dashboard/vite/tests/fixtures-json-pkg/package.json create mode 100644 packages/dashboard/vite/tests/fixtures-json-pkg/vendure-config.ts create mode 100644 packages/dashboard/vite/tests/fixtures-json/config-data.json create mode 100644 packages/dashboard/vite/tests/fixtures-json/my-plugin/src/my.plugin.ts create mode 100644 packages/dashboard/vite/tests/fixtures-json/my-plugin/src/plugin-data.json create mode 100644 packages/dashboard/vite/tests/fixtures-json/package.json create mode 100644 packages/dashboard/vite/tests/fixtures-json/vendure-config.ts create mode 100644 packages/dashboard/vite/tests/json-imports.spec.ts 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/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..ad8c2ccec3f --- /dev/null +++ b/packages/dashboard/vite/tests/json-imports.spec.ts @@ -0,0 +1,78 @@ +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' }); + }); + // 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..5a634245fdb 100644 --- a/packages/dashboard/vite/utils/compiler.ts +++ b/packages/dashboard/vite/utils/compiler.ts @@ -325,12 +325,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 +355,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 +412,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 +458,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. @@ -455,11 +478,20 @@ 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') 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); From cc420181df0f7c1969405a00e44d7bf0bc3c87b2 Mon Sep 17 00:00:00 2001 From: YuChao Liang Date: Wed, 9 Sep 2026 22:05:39 +0900 Subject: [PATCH 46/70] fix(dashboard): correct "State" translation across locales to mean condition (#5302) Co-authored-by: Ihor Barmak Co-authored-by: Anh Bui Quang Co-authored-by: vendure-ci-automation-bot[bot] <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Co-authored-by: Will Nahmens <63622091+biggamesmallworld@users.noreply.github.com> Co-authored-by: Michael Bromley Co-authored-by: Michael Bromley Co-authored-by: Michael Bromley --- packages/dashboard/src/i18n/locales/ne.po | 2 +- packages/dashboard/src/i18n/locales/nl.po | 2 +- packages/dashboard/src/i18n/locales/ru.po | 2 +- packages/dashboard/src/i18n/locales/sv.po | 2 +- packages/dashboard/src/i18n/locales/tr.po | 2 +- packages/dashboard/src/i18n/locales/uk.po | 2 +- packages/dashboard/src/i18n/locales/zh_Hans.po | 2 +- packages/dashboard/src/i18n/locales/zh_Hant.po | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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 From 518cd16aab1d509f24c3d39228d17f19195088d1 Mon Sep 17 00:00:00 2001 From: Will Nahmens <63622091+biggamesmallworld@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:17:50 +0200 Subject: [PATCH 47/70] fix(core): sort by localeText custom fields (#5265) --- packages/core/e2e/custom-fields.e2e-spec.ts | 18 +++++++++++++ .../parse-sort-params.spec.ts | 25 +++++++++++++++++++ .../list-query-builder/parse-sort-params.ts | 7 +++--- 3 files changed, 47 insertions(+), 3 deletions(-) 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/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); From 6cc2ca37d06888b10490243da9d157c5b9958724 Mon Sep 17 00:00:00 2001 From: ISMATULLA <66904522+Ismatulla@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:35:33 +0200 Subject: [PATCH 48/70] =?UTF-8?q?fix(dashboard):=20show=20field=20name=20i?= =?UTF-8?q?nstead=20of=20form=20path=20as=20custom=20field=20=E2=80=A6=20(?= =?UTF-8?q?#5313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: Ihor Barmak Co-authored-by: Anh Bui Quang Co-authored-by: vendure-ci-automation-bot[bot] <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Co-authored-by: Will Nahmens <63622091+biggamesmallworld@users.noreply.github.com> Co-authored-by: Michael Bromley Co-authored-by: StefanOrzu Co-authored-by: Kevin Co-authored-by: Drayke Co-authored-by: thestreamcode Co-authored-by: wakqasahmed Co-authored-by: Deepak Maharana <100968930+deep231w@users.noreply.github.com> Co-authored-by: Housein Abo Shaar <76689341+HouseinIsProgramming@users.noreply.github.com> Co-authored-by: Housein Abo Shaar <76689341+GogoIsProgramming@users.noreply.github.com> Co-authored-by: ryrahul Co-authored-by: Will Nahmens Co-authored-by: Claude Co-authored-by: Michael Bromley Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Höck Co-authored-by: Michael Bromley --- .../e2e/fixtures/e2e-shared-config.ts | 7 ++++++ .../e2e/tests/catalog/custom-fields.spec.ts | 12 ++++++++++ .../components/shared/custom-fields-form.tsx | 24 ++++++++++++------- 3 files changed, 34 insertions(+), 9 deletions(-) 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/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)} From 91939b1effc7e5a4ef7ce8c5c04f4e7086ea36eb Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Wed, 9 Sep 2026 16:11:07 +0200 Subject: [PATCH 49/70] fix(core): detect schema drift by tables rather than migration history An empty `migrations` table does not mean an empty database. A schema built with `synchronize: true` has every table and no migration history, because TypeORM only writes that table when it runs a migration. The previous guard treated the two states as the same, so switching to `synchronize: false` and changing an entity produced "No pending migrations found" instead of the ALTER TABLE the developer needs to generate a migration for. The guard now asks whether any entity table exists. Also: - `describeDiagnostic()` no longer claims all patterns resolve against the working directory when the `migrations` config mixes absolute and relative patterns. - `connection.close()` and `resetConfig()` now run even if an `onDiagnostic` callback throws. Relates to #5001 Co-Authored-By: Claude Opus 5 --- packages/cli/e2e/migrate-command.e2e-spec.ts | 32 +++++++++++++- .../commands/migrate/migration-report.spec.ts | 18 ++++++++ packages/core/src/migrate.ts | 42 +++++++++++++------ 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/packages/cli/e2e/migrate-command.e2e-spec.ts b/packages/cli/e2e/migrate-command.e2e-spec.ts index 17d994519fd..d052c3a7eb7 100644 --- a/packages/cli/e2e/migrate-command.e2e-spec.ts +++ b/packages/cli/e2e/migrate-command.e2e-spec.ts @@ -203,8 +203,8 @@ describe( expect(result.success).toBe(true); expect(result.message).toContain('No pending migrations found'); - // A database with no migration history is not "out of sync": every table is - // pending because nothing has been applied yet. + // 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); @@ -252,6 +252,34 @@ describe( expect(result.message).not.toContain('No pending migrations found'); }); + // #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); diff --git a/packages/cli/src/commands/migrate/migration-report.spec.ts b/packages/cli/src/commands/migrate/migration-report.spec.ts index 579b42ff060..977a07be08b 100644 --- a/packages/cli/src/commands/migrate/migration-report.spec.ts +++ b/packages/cli/src/commands/migrate/migration-report.spec.ts @@ -59,6 +59,24 @@ describe('buildMigrationReport()', () => { expect(report.details).not.toContain('/elsewhere'); }); + it('mentions the working directory only as it applies when patterns are mixed', () => { + const report = buildMigrationReport( + [], + [ + { + type: 'no-migrations-matched', + patterns: ['/project/dist/migrations/*.js', 'migrations/*.ts'], + cwd: '/elsewhere', + }, + ], + ); + + expect(report.details).toContain('/project/dist/migrations/*.js'); + expect(report.details).toContain( + 'Relative patterns are resolved against the current directory (/elsewhere)', + ); + }); + it('caps the rendered drift queries', () => { const queries = Array.from({ length: 25 }, (_, i) => `CREATE TABLE \`t${i}\` (id int)`); const report = buildMigrationReport([], [{ type: 'schema-out-of-sync', queries }]); diff --git a/packages/core/src/migrate.ts b/packages/core/src/migrate.ts index 3a5fa37e682..c589ef9d860 100644 --- a/packages/core/src/migrate.ts +++ b/packages/core/src/migrate.ts @@ -137,19 +137,26 @@ export async function runMigrations( process.exitCode = 1; } } finally { - await checkMigrationStatus(connection, report); - await connection.close(); - resetConfig(); + try { + await checkMigrationStatus(connection, report); + } finally { + await connection.close(); + resetConfig(); + } } return migrationsRan; } async function checkMigrationStatus(connection: Connection, report: (d: MigrationDiagnostic) => void) { - // Against a database with no migration history the schema builder reports the entire schema - // as pending, which is what a project looks like before its first migration is applied. That - // is not drift, so there is nothing useful to say about it. - const executed = await new MigrationExecutor(connection).getExecutedMigrations(); - if (!executed.length) { + // 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 hasAnyEntityTable(connection))) { return; } const builderLog = await connection.driver.createSchemaBuilder().log(); @@ -158,6 +165,16 @@ async function checkMigrationStatus(connection: Connection, report: (d: Migratio } } +async function hasAnyEntityTable(connection: Connection): Promise { + const queryRunner = connection.createQueryRunner(); + try { + const tables = await queryRunner.getTables(connection.entityMetadatas.map(m => m.tablePath)); + return tables.length > 0; + } finally { + await queryRunner.release(); + } +} + /** * The full drift list can be the entire schema, which is more than a terminal should be asked * to render. The queries themselves remain on the diagnostic for programmatic consumers. @@ -177,13 +194,14 @@ export function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] { // 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 relative = diagnostic.patterns.some(pattern => !path.isAbsolute(pattern)); + const anyRelative = diagnostic.patterns.some(pattern => !path.isAbsolute(pattern)); return [ 'No migration files matched the configured `migrations` patterns, but this database has migrations recorded as applied.', - relative - ? `Patterns are resolved relative to the current directory (${diagnostic.cwd}):` - : 'Nothing on disk matches these patterns. If they point at compiled output, check that it has been built:', + 'Nothing on disk matches these patterns. If they point at compiled output, check that it has been built:', ...diagnostic.patterns.map(pattern => ' - ' + pattern), + ...(anyRelative + ? [`Relative patterns are resolved against the current directory (${diagnostic.cwd}).`] + : []), ]; } case 'schema-out-of-sync': { From 166793131b3201cfb43560186096d7ec90f29fea Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Wed, 9 Sep 2026 16:30:54 +0200 Subject: [PATCH 50/70] fix(core): simplify register custom field handling, isolate its test config Address review feedback on #5180. `RegisterCustomerInput` is now registered in `createInputsWithCustomFields` in the constructor, so `hasCustomFields()` and `shouldApplyDefaults()` pick it up unchanged. `getEntityNameFromInputType()` maps it to `Customer`. This replaces the separate `nonStandardInputEntityNames` map, which stated "this is a create input" in a fourth place alongside the two sets and `OrderLineCustomFieldsInput`. The test config moves out of `custom-fields.e2e-spec.ts` into its own spec. Customer there has only the readonly `score` field, which is what makes `writeableNonLocalizedFields` emit the `customFields: JSON` fallback shape on `UpdateCustomerInput`, which in turn is what lets "throws on attempt to update readonly field when no other custom fields defined" reach the interceptor. Adding a writable Customer custom field generates `UpdateCustomerCustomFieldsInput` instead and breaks that test, which is the only coverage of the JSON-fallback readonly path. The new spec also covers what was missing: that a rejected value leaves no Customer behind, that a valid value is persisted on the entity, and that defaults are applied on registration now that it counts as a create input. Relates to #5180 Co-Authored-By: Claude Opus 5 --- ...custom-field-register-customer.e2e-spec.ts | 152 ++++++++++++++++++ packages/core/e2e/custom-fields.e2e-spec.ts | 50 ------ .../custom-field-processing-interceptor.ts | 32 +--- 3 files changed, 156 insertions(+), 78 deletions(-) create mode 100644 packages/core/e2e/custom-field-register-customer.e2e-spec.ts 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..3ac8fd890b8 --- /dev/null +++ b/packages/core/e2e/custom-field-register-customer.e2e-spec.ts @@ -0,0 +1,152 @@ +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'; +import { assertThrowsWithMessage } from './utils/assert-throws-with-message'; + +/** + * 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( + 'throws if a custom field value is invalid', + assertThrowsWithMessage(async () => { + await shopClient.query(registerDocument, { + emailAddress: 'register-validation-invalid@test.com', + value: 'nope', + }); + }, "The value ['nope'] is not valid"), + ); + + it('does not create an account when validation fails', async () => { + 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.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 64a52a96a05..bddace0ee2d 100644 --- a/packages/core/e2e/custom-fields.e2e-spec.ts +++ b/packages/core/e2e/custom-fields.e2e-spec.ts @@ -201,18 +201,6 @@ const customConfig = mergeConfig(testConfig(), { type: 'int', readonly: true, }, - { - // Public and writable, so it is added to RegisterCustomerInput by - // addRegisterCustomerCustomFieldsInput(). - name: 'validateOnRegister', - type: 'string', - nullable: true, - validate: value => { - if (typeof value === 'string' && value !== 'valid') { - return `The value ['${value}'] is not valid`; - } - }, - }, ], Collection: [ { name: 'secretKey1', type: 'string', defaultValue: '', public: false, internal: true }, @@ -515,30 +503,6 @@ describe('Custom fields', () => { }, 'The custom field "score" is readonly'), ); - describe('validation of custom fields on RegisterCustomerInput', () => { - it( - 'throws if a custom field value is invalid', - assertThrowsWithMessage(async () => { - await shopClient.query(registerWithCustomFieldsDocument, { - emailAddress: 'register-validation-invalid@test.com', - value: 'nope', - }); - }, "The value ['nope'] is not valid"), - ); - - it('succeeds if the custom field value is valid', async () => { - const { registerCustomerAccount } = await shopClient.query( - registerWithCustomFieldsDocument, - { - emailAddress: 'register-validation-valid@test.com', - value: 'valid', - }, - ); - - expect(registerCustomerAccount).toEqual({ success: true }); - }); - }); - it( 'throws on attempt to create readonly field', assertThrowsWithMessage(async () => { @@ -1593,20 +1557,6 @@ const getProductsFilterByIntListDocument = graphql(` } `); -const registerWithCustomFieldsDocument = graphqlShop(` - mutation RegisterWithCustomFields($emailAddress: String!, $value: String!) { - registerCustomerAccount(input: { emailAddress: $emailAddress, customFields: { validateOnRegister: $value } }) { - ... on Success { - success - } - ... on ErrorResult { - errorCode - message - } - } - } -`); - const addItemToOrderWithCustomFieldsDocument = graphqlShop(` mutation AddItemToOrderWithCustomFields($productVariantId: ID!, $quantity: Int!, $validateInt: Int!) { addItemToOrder(productVariantId: $productVariantId, quantity: $quantity, customFields: { validateInt: $validateInt }) { 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 cf26805742f..91a6ee11c3d 100644 --- a/packages/core/src/api/middleware/custom-field-processing-interceptor.ts +++ b/packages/core/src/api/middleware/custom-field-processing-interceptor.ts @@ -33,20 +33,6 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { private readonly createInputsWithCustomFields = new Set(); private readonly updateInputsWithCustomFields = new Set(); - /** - * Inputs which carry custom fields but do not follow the - * `CreateInput` / `UpdateInput` naming convention, mapped to the - * entity whose custom field config governs them. - * - * `RegisterCustomerInput` gains a `customFields` field from - * `addRegisterCustomerCustomFieldsInput()` whenever the Customer entity has - * public, writable custom fields - so without this entry those fields would be - * settable at registration but never validated or defaulted. - */ - private static readonly nonStandardInputEntityNames: { [inputName: string]: string } = { - RegisterCustomerInput: 'Customer', - }; - constructor( private readonly configService: ConfigService, private readonly moduleRef: ModuleRef, @@ -55,6 +41,8 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { this.createInputsWithCustomFields.add(`Create${entityName}Input`); this.updateInputsWithCustomFields.add(`Update${entityName}Input`); }); + // RegisterCustomerInput carries Customer custom fields but is not named CreateCustomerInput. + this.createInputsWithCustomFields.add('RegisterCustomerInput'); // Note: OrderLineCustomFieldsInput is handled separately since it's used in both // create operations (addItemToOrder) and update operations (adjustOrderLine) } @@ -97,16 +85,10 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { return ( this.createInputsWithCustomFields.has(typeName) || this.updateInputsWithCustomFields.has(typeName) || - this.isNonStandardInputWithCustomFields(typeName) || typeName === 'OrderLineCustomFieldsInput' ); } - private isNonStandardInputWithCustomFields(typeName: string): boolean { - const entityName = CustomFieldProcessingInterceptor.nonStandardInputEntityNames[typeName]; - return !!entityName && !!this.configService.customFields[entityName as keyof CustomFields]; - } - private async processInputVariables( typeName: string, variableInput: any, @@ -131,11 +113,6 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { return true; } - // RegisterCustomerInput creates a Customer, so it behaves as a create input. - if (this.isNonStandardInputWithCustomFields(typeName)) { - return true; - } - // For OrderLineCustomFieldsInput, check the actual mutation name if (typeName === 'OrderLineCustomFieldsInput') { return this.isOrderLineCreateOperation(operation); @@ -242,9 +219,8 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { } private getEntityNameFromInputType(typeName: string): string { - const nonStandard = CustomFieldProcessingInterceptor.nonStandardInputEntityNames[typeName]; - if (nonStandard) { - return nonStandard; + if (typeName === 'RegisterCustomerInput') { + return 'Customer'; } // Remove "Create" or "Update" prefix and "Input" suffix // e.g., "CreateProductInput" -> "Product", "UpdateCustomerInput" -> "Customer" From b7983fe53ef64f430399afbb4dc20a79daaaae81 Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Wed, 9 Sep 2026 16:31:13 +0200 Subject: [PATCH 51/70] fix(core): disable orphan-nullification of surcharges on Order save `Surcharge.order` used TypeORM's default `orphanedRowAction: 'nullify'`, so saving an Order set `orderId` to null on every persisted Surcharge row missing from that Order's `surcharges` array. Callers hold that array as a snapshot taken when the Order was loaded, so a Surcharge added since - by a blocking event handler, or by a concurrent request - was silently detached: no error, no event, no log. Omitting `surcharges` from the save in `applyPriceAdjustments` closes the path this was reported on, but the same hazard is present in every other full-Order save: `removeItemsFromOrder`, `addCustomerToOrder`, `PromotionService.addPromotionsToOrder`, `OrderModifier.cancelOrder`, and plugin code following the pattern documented on `ActiveOrderStrategy`. Setting `orphanedRowAction: 'disable'` on the relation fixes all of them at once. Nothing relies on the orphan behaviour: surcharges are deleted by `removeSurchargeFromOrder`, not detached. `orphanedRowAction` is applied when TypeORM builds the persistence subjects, so this changes no schema and needs no migration. Co-Authored-By: Claude Opus 5 --- .../typescript-api/entities/order.mdx | 5 ++ .../typescript-api/entities/surcharge.mdx | 5 +- packages/core/e2e/order.e2e-spec.ts | 52 +++++++++++++++++++ .../core/src/entity/order/order.entity.ts | 5 ++ .../src/entity/surcharge/surcharge.entity.ts | 10 +++- 5 files changed, 75 insertions(+), 2 deletions(-) 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/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/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/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() From 235bcd7b1008ec4c927c4221b158948cfeb0cd2b Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Wed, 9 Sep 2026 16:53:28 +0200 Subject: [PATCH 52/70] test(core): make the register custom field tests order-independent Address Nigel review of #5180. "does not create an account when validation fails" performed no registration of its own, so it only passed because the preceding test had attempted one. Run alone it asserted that an email nobody submitted has no Customer. Folded into the test that attempts the registration. Also guards the `items[0]` access in the defaults test with a `totalItems` assertion, and separates the new constructor comment from the pre-existing `OrderLineCustomFieldsInput` note so they no longer read as one block. Relates to #5180 Co-Authored-By: Claude Opus 5 --- .../custom-field-register-customer.e2e-spec.ts | 15 ++++++--------- .../custom-field-processing-interceptor.ts | 7 +++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/e2e/custom-field-register-customer.e2e-spec.ts b/packages/core/e2e/custom-field-register-customer.e2e-spec.ts index 3ac8fd890b8..3769badfb43 100644 --- a/packages/core/e2e/custom-field-register-customer.e2e-spec.ts +++ b/packages/core/e2e/custom-field-register-customer.e2e-spec.ts @@ -8,7 +8,6 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf import { graphql } from './graphql/graphql-admin'; import { graphql as graphqlShop } from './graphql/graphql-shop'; -import { assertThrowsWithMessage } from './utils/assert-throws-with-message'; /** * These tests need their own config because `custom-fields.e2e-spec.ts` relies on Customer @@ -102,17 +101,14 @@ describe('Custom fields on RegisterCustomerInput', () => { await server.destroy(); }); - it( - 'throws if a custom field value is invalid', - assertThrowsWithMessage(async () => { - await shopClient.query(registerDocument, { + it('rejects an invalid value and creates no account', async () => { + await expect( + shopClient.query(registerDocument, { emailAddress: 'register-validation-invalid@test.com', value: 'nope', - }); - }, "The value ['nope'] is not valid"), - ); + }), + ).rejects.toThrow("The value ['nope'] is not valid"); - it('does not create an account when validation fails', async () => { const { customers } = await adminClient.query(getCustomerByEmailDocument, { emailAddress: 'register-validation-invalid@test.com', }); @@ -147,6 +143,7 @@ describe('Custom fields on RegisterCustomerInput', () => { emailAddress: 'register-default@test.com', }); + expect(customers.totalItems).toBe(1); expect(customers.items[0].customFields.defaultOnRegister).toBe('the-default'); }); }); 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 91a6ee11c3d..14055875e01 100644 --- a/packages/core/src/api/middleware/custom-field-processing-interceptor.ts +++ b/packages/core/src/api/middleware/custom-field-processing-interceptor.ts @@ -41,10 +41,12 @@ export class CustomFieldProcessingInterceptor implements NestInterceptor { this.createInputsWithCustomFields.add(`Create${entityName}Input`); this.updateInputsWithCustomFields.add(`Update${entityName}Input`); }); - // RegisterCustomerInput carries Customer custom fields but is not named CreateCustomerInput. - this.createInputsWithCustomFields.add('RegisterCustomerInput'); // 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) { @@ -220,6 +222,7 @@ 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 From ab2fbf116c7d9f315f63ccf7cb0c821194cb2c1a Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:02:29 +0000 Subject: [PATCH 53/70] docs: Regenerate manifest.json --- docs/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 37d6ad27eb3..a0be9f425bb 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", From 5a0ef356870faa79a305e11b923a1c20fe017e87 Mon Sep 17 00:00:00 2001 From: Gabriel Bui Date: Wed, 9 Sep 2026 17:06:32 +0200 Subject: [PATCH 54/70] test(core): use PDF bytes and check the persisted Channel assignment Three changes to the new createFromFileStream test: - Stream real PDF bytes under the .pdf filename. Plain text has no magic-byte signature, so content detection returned nothing and the file passed permission checks on the extension alone. If content detection is ever tightened, the test would fail with a MimeTypeError that looks unrelated to the behaviour it guards. Passing a Buffer also takes the same byte path as the real caller in AssetImporter, instead of the object-mode string path. - Read the Asset back with its channels relation and assert on that too. The returned entity carries the ids assigned in memory before the save, so on its own it cannot show the join row was written. The in-memory assertion stays, because it is what catches a missing ctx.channelId. - Move the ordering note so it covers both trailing describe blocks. Each creates an Asset, which advances the auto-increment id that the total-count and hardcoded-id assertions earlier in the file rely on. Relates to #5204 Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/e2e/asset.e2e-spec.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/core/e2e/asset.e2e-spec.ts b/packages/core/e2e/asset.e2e-spec.ts index dd8bec5e94d..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 { Asset, AssetService, ChannelService, 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 @@ -639,7 +640,8 @@ describe('Asset resolver', () => { }); }); - // https://github.com/vendurehq/vendure/issues/4651 + // 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); @@ -648,7 +650,7 @@ describe('Asset resolver', () => { const defaultChannel = await server.app.get(ChannelService).getDefaultChannel(); const result = await assetService.createFromFileStream( - Readable.from(['test file content']), + Readable.from([Buffer.from('%PDF-1.4\n%%EOF\n')]), 'test-file.pdf', ); @@ -657,7 +659,13 @@ describe('Asset resolver', () => { 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]); }); }); }); From 03ba1f42ebc09184d0983c01e3ab49c930be9fd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:37:22 +0200 Subject: [PATCH 55/70] chore(deps): bump @rollup/rollup-linux-x64-gnu from 4.55.1 to 4.63.1 (#5307) --- bun.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 9f5e26b59e7..64de9050bf2 100644 --- a/bun.lock +++ b/bun.lock @@ -1987,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=="], @@ -6929,6 +6929,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=="], From 6407613741eea65dc41e82d1ec26bda6ab148c86 Mon Sep 17 00:00:00 2001 From: Alexander Scholz Date: Wed, 9 Sep 2026 22:48:08 +0200 Subject: [PATCH 56/70] fix(dashboard): extend order detail query when pre-loading order details (#5173) Co-authored-by: Michael Bromley --- .../utils/order-detail-loaders.spec.ts | 159 ++++++++++++++++++ .../_orders/utils/order-detail-loaders.tsx | 50 ++++-- 2 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.spec.ts 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`, }); From 4225c577d449d5cc006663e25f8962bc12673857 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 9 Sep 2026 22:51:50 +0200 Subject: [PATCH 57/70] docs: note that field resolvers only get guards and how to hook per-field logic (#5336) Co-authored-by: Ihor Barmak --- .../developer-guide/the-api-layer/index.mdx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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 From de9bdac9b9002259efe523ecbbea38e3127a63df Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:52:26 +0000 Subject: [PATCH 58/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index a0be9f425bb..921710673e1 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", From 539fae74431bbc1bb71ccce3d3bbfbcee3d11bc0 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 9 Sep 2026 22:54:39 +0200 Subject: [PATCH 59/70] fix(dashboard): warn when a package.json import is skipped (#5329) --- .../type-only-vendure-config.ts | 16 ++++++ .../dashboard/vite/tests/json-imports.spec.ts | 34 +++++++++++ packages/dashboard/vite/utils/compiler.ts | 57 +++++++++++++++++-- 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard/vite/tests/fixtures-json-pkg/type-only-vendure-config.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/json-imports.spec.ts b/packages/dashboard/vite/tests/json-imports.spec.ts index ad8c2ccec3f..6594396a9bb 100644 --- a/packages/dashboard/vite/tests/json-imports.spec.ts +++ b/packages/dashboard/vite/tests/json-imports.spec.ts @@ -54,6 +54,40 @@ describe('compiling a config which imports .json files', () => { 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. diff --git a/packages/dashboard/vite/utils/compiler.ts b/packages/dashboard/vite/utils/compiler.ts index 5a634245fdb..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( @@ -468,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); @@ -483,7 +498,10 @@ async function collectLocalSourceFiles( // 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') return; + if (path.basename(resolved) === 'package.json') { + skippedPackageJson.add(resolved); + return; + } if (visited.has(resolved)) return; visited.add(resolved); @@ -499,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; @@ -521,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: { From aab77e7be792bd5597dfa4748b7a9f2df9e8be17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:55:39 +0200 Subject: [PATCH 60/70] chore(deps): bump the github-actions group with 4 updates (#5303) --- .github/workflows/floating_deps_check.yml | 6 +++--- .github/workflows/generate_docs.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/floating_deps_check.yml b/.github/workflows/floating_deps_check.yml index 5771c063f1e..29527b9eebe 100644 --- a/.github/workflows/floating_deps_check.yml +++ b/.github/workflows/floating_deps_check.yml @@ -32,7 +32,7 @@ jobs: contents: read issues: write steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - 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. @@ -41,7 +41,7 @@ jobs: # .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@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 @@ -90,7 +90,7 @@ jobs: # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: floating-deps-${{ github.run_id }} path: | diff --git a/.github/workflows/generate_docs.yml b/.github/workflows/generate_docs.yml index bc8c59914d1..1da861f6637 100644 --- a/.github/workflows/generate_docs.yml +++ b/.github/workflows/generate_docs.yml @@ -103,7 +103,7 @@ jobs: if: steps.diff.outputs.changed == 'true' id: ai-message continue-on-error: true - uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1 + uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3 with: model: openai/gpt-4o-mini max-tokens: 100 From 818327d1ccbf682ed5748d35ee7a764288e223fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:56:13 +0200 Subject: [PATCH 61/70] chore(deps): bump the linting group with 2 updates (#5304) --- bun.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 64de9050bf2..f311af18a55 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -410,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", @@ -2589,25 +2589,25 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.68.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.68.0", "@typescript-eslint/type-utils": "8.68.0", "@typescript-eslint/utils": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.68.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q=="], + "@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.68.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.68.0", "@typescript-eslint/types": "^8.68.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw=="], + "@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@8.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.0" } }, "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA=="], + "@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.68.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw=="], + "@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@8.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "@typescript-eslint/typescript-estree": "8.68.0", "@typescript-eslint/utils": "8.68.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-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg=="], + "@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@8.68.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/typescript-estree": "8.68.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ=="], + "@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@8.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A=="], + "@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=="], @@ -3467,7 +3467,7 @@ "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.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-vG7yLURXNvCHy0FBdbZRwIu0BLPJMlUUJS2Ep7ud9w1YCLftFZtuEjyjhym0Qq9yuZ6LJUitNlu/hMk0gakXAw=="], "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=="], @@ -6169,23 +6169,23 @@ "@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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + "@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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + "@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.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.68.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.68.0", "@typescript-eslint/tsconfig-utils": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.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-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA=="], + "@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/@typescript-eslint/types": ["@typescript-eslint/types@8.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + "@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.68.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.68.0", "@typescript-eslint/tsconfig-utils": "8.68.0", "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.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-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA=="], + "@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/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.68.0", "", {}, "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg=="], + "@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=="], From 6c3c560564bf2cd1aa4fe111b3263163eed1c029 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 10 Sep 2026 10:21:02 +0200 Subject: [PATCH 62/70] chore: restrict Dependabot major updates to development tooling (#5340) --- .github/dependabot.yml | 176 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) 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/*' From 9a1dbe79400f11c9b5249d54858394c2729f90c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:18:06 +0200 Subject: [PATCH 63/70] chore(deps-dev): bump the linting group with 2 updates (#5341) --- bun.lock | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index f311af18a55..4729f7dc47c 100644 --- a/bun.lock +++ b/bun.lock @@ -3467,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.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-vG7yLURXNvCHy0FBdbZRwIu0BLPJMlUUJS2Ep7ud9w1YCLftFZtuEjyjhym0Qq9yuZ6LJUitNlu/hMk0gakXAw=="], + "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=="], @@ -6391,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=="], @@ -7759,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=="], @@ -8605,18 +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=="], - "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=="], @@ -8869,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=="], @@ -9003,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=="], From 0591b900cd2af0ace2f02a2c5695c29a3f9d102d Mon Sep 17 00:00:00 2001 From: Will Nahmens Date: Thu, 10 Sep 2026 12:56:42 +0200 Subject: [PATCH 64/70] fix(core): narrow the migration diagnostic surface to two exports The two diagnostic shapes carried structured data that nothing acted on: `patterns` and `cwd` decided whether to print one line, `queries` was sliced and prefixed. Both were only ever turned into text. Collapse them into a single `MigrationDiagnostic` carrying a `code` and the rendered `lines`, so the package exports two names rather than five. Adding a field later is easy; removing an exported type is not. Core now renders the wording where it detects the condition, and `describeDiagnostic` is gone. The ten-item cap moves with the rendering to the CLI, so a deploy script calling `runMigrations()` directly gets the full drift list back. `hasAnyEntityTable` loaded every entity table to answer a yes/no question, which `createSchemaBuilder().log()` then did again on its own query runner. `hasTable` on one core table answers it in a single statement. Relates to #5001 Co-Authored-By: Claude Opus 5 --- .../migration/describe-diagnostic.mdx | 17 --- .../migration/generate-migration.mdx | 2 +- .../migration/migration-diagnostic.mdx | 29 +++- .../migration/migration-options.mdx | 2 +- .../no-migrations-matched-diagnostic.mdx | 38 ----- .../migration/revert-last-migration.mdx | 2 +- .../migration/run-migrations-options.mdx | 2 +- .../migration/run-migrations.mdx | 2 +- .../schema-out-of-sync-diagnostic.mdx | 31 ---- docs/manifest.json | 18 --- packages/cli/e2e/migrate-command.e2e-spec.ts | 3 + .../commands/migrate/migration-report.spec.ts | 53 ++----- .../src/commands/migrate/migration-report.ts | 18 ++- packages/core/src/index.ts | 9 +- packages/core/src/migrate.ts | 139 +++++++----------- 15 files changed, 116 insertions(+), 249 deletions(-) delete mode 100644 docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx delete mode 100644 docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx delete mode 100644 docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx diff --git a/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx deleted file mode 100644 index 189156f6c73..00000000000 --- a/docs/docs/reference/typescript-api/migration/describe-diagnostic.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "DescribeDiagnostic" -generated: true ---- - - -Renders a [MigrationDiagnostic](/reference/typescript-api/migration/migration-diagnostic#migrationdiagnostic) as lines of human-readable text. - -```ts title="Signature" -function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] -``` -Parameters - -### diagnostic - -MigrationDiagnostic`} /> - diff --git a/docs/docs/reference/typescript-api/migration/generate-migration.mdx b/docs/docs/reference/typescript-api/migration/generate-migration.mdx index 21b20e5574d..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 index 0480a5d4f3f..04fbdd24da0 100644 --- a/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx +++ b/docs/docs/reference/typescript-api/migration/migration-diagnostic.mdx @@ -2,11 +2,36 @@ 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" -type MigrationDiagnostic = NoMigrationsMatchedDiagnostic | SchemaOutOfSyncDiagnostic +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/no-migrations-matched-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx deleted file mode 100644 index 01e76c16972..00000000000 --- a/docs/docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "NoMigrationsMatchedDiagnostic" -generated: true ---- - - -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. - -```ts title="Signature" -interface NoMigrationsMatchedDiagnostic { - type: 'no-migrations-matched'; - patterns: string[]; - cwd: string; -} -``` - -
- -### type - - - - -### patterns - - - -The glob patterns configured in `dbConnectionOptions.migrations`. -### cwd - - - -The directory the patterns were resolved against. - - -
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 55a8a2f3bfa..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 index d69f021855e..cc36b4c206a 100644 --- a/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx +++ b/docs/docs/reference/typescript-api/migration/run-migrations-options.mdx @@ -2,7 +2,7 @@ title: "RunMigrationsOptions" generated: true --- - + Options for [runMigrations](/reference/typescript-api/migration/run-migrations#runmigrations). diff --git a/docs/docs/reference/typescript-api/migration/run-migrations.mdx b/docs/docs/reference/typescript-api/migration/run-migrations.mdx index d40f5e07211..d78b24d2d66 100644 --- a/docs/docs/reference/typescript-api/migration/run-migrations.mdx +++ b/docs/docs/reference/typescript-api/migration/run-migrations.mdx @@ -2,7 +2,7 @@ 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. diff --git a/docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx b/docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx deleted file mode 100644 index c23701f98bf..00000000000 --- a/docs/docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "SchemaOutOfSyncDiagnostic" -generated: true ---- - - -The database schema does not match the current entity configuration, so a new migration -needs to be generated. - -```ts title="Signature" -interface SchemaOutOfSyncDiagnostic { - type: 'schema-out-of-sync'; - queries: string[]; -} -``` - -
- -### type - - - - -### queries - - - -The SQL statements which would bring the schema back into line with the configuration. - - -
diff --git a/docs/manifest.json b/docs/manifest.json index d597a44986d..a0b93ff7fd7 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2286,12 +2286,6 @@ "slug": "migration", "file": "docs/reference/typescript-api/migration/index.mdx", "children": [ - { - "title": "DescribeDiagnostic", - "slug": "describe-diagnostic", - "file": "docs/reference/typescript-api/migration/describe-diagnostic.mdx", - "lastModified": "2026-09-01T08:40:40Z" - }, { "title": "GenerateMigration", "slug": "generate-migration", @@ -2322,12 +2316,6 @@ "file": "docs/reference/typescript-api/migration/migration-options.mdx", "lastModified": "2026-01-28T14:49:21+01:00" }, - { - "title": "NoMigrationsMatchedDiagnostic", - "slug": "no-migrations-matched-diagnostic", - "file": "docs/reference/typescript-api/migration/no-migrations-matched-diagnostic.mdx", - "lastModified": "2026-08-25T09:10:29Z" - }, { "title": "RescaleOrderLinePromotionAdjustments", "slug": "rescale-order-line-promotion-adjustments", @@ -2351,12 +2339,6 @@ "slug": "run-migrations-options", "file": "docs/reference/typescript-api/migration/run-migrations-options.mdx", "lastModified": "2026-09-01T08:40:40Z" - }, - { - "title": "SchemaOutOfSyncDiagnostic", - "slug": "schema-out-of-sync-diagnostic", - "file": "docs/reference/typescript-api/migration/schema-out-of-sync-diagnostic.mdx", - "lastModified": "2026-08-25T09:10:29Z" } ], "lastModified": "2026-01-28T14:49:21+01:00" diff --git a/packages/cli/e2e/migrate-command.e2e-spec.ts b/packages/cli/e2e/migrate-command.e2e-spec.ts index d052c3a7eb7..6581556460b 100644 --- a/packages/cli/e2e/migrate-command.e2e-spec.ts +++ b/packages/cli/e2e/migrate-command.e2e-spec.ts @@ -250,6 +250,9 @@ describe( 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 diff --git a/packages/cli/src/commands/migrate/migration-report.spec.ts b/packages/cli/src/commands/migrate/migration-report.spec.ts index 977a07be08b..193d22037fb 100644 --- a/packages/cli/src/commands/migrate/migration-report.spec.ts +++ b/packages/cli/src/commands/migrate/migration-report.spec.ts @@ -4,13 +4,15 @@ import { describe, expect, it } from 'vitest'; import { buildMigrationReport } from './migration-report'; const unmatched: MigrationDiagnostic = { - type: 'no-migrations-matched', - patterns: ['dist/migrations/*.js'], - cwd: '/project', + code: 'no-migrations-matched', + lines: ['No migration files matched the configured `migrations` patterns.', ' - dist/migrations/*.js'], }; const outOfSync: MigrationDiagnostic = { - type: 'schema-out-of-sync', - queries: ['ALTER TABLE `product` ADD `foo` varchar(255)'], + code: 'schema-out-of-sync', + lines: [ + 'Your database schema does not match your current configuration.', + ' - ALTER TABLE `product` ADD `foo` varchar(255)', + ], }; describe('buildMigrationReport()', () => { @@ -36,7 +38,6 @@ describe('buildMigrationReport()', () => { 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.details).toContain('/project'); expect(report.hasWarnings).toBe(true); }); @@ -50,40 +51,14 @@ describe('buildMigrationReport()', () => { expect(report.hasWarnings).toBe(true); }); - it('does not blame the working directory when the patterns are absolute', () => { - const report = buildMigrationReport([], [ - { type: 'no-migrations-matched', patterns: ['/project/dist/migrations/*.js'], cwd: '/elsewhere' }, - ]); + // 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('/project/dist/migrations/*.js'); - expect(report.details).not.toContain('/elsewhere'); - }); - - it('mentions the working directory only as it applies when patterns are mixed', () => { - const report = buildMigrationReport( - [], - [ - { - type: 'no-migrations-matched', - patterns: ['/project/dist/migrations/*.js', 'migrations/*.ts'], - cwd: '/elsewhere', - }, - ], - ); - - expect(report.details).toContain('/project/dist/migrations/*.js'); - expect(report.details).toContain( - 'Relative patterns are resolved against the current directory (/elsewhere)', - ); - }); - - it('caps the rendered drift queries', () => { - const queries = Array.from({ length: 25 }, (_, i) => `CREATE TABLE \`t${i}\` (id int)`); - const report = buildMigrationReport([], [{ type: 'schema-out-of-sync', queries }]); - - expect(report.details).toContain('CREATE TABLE `t9` (id int)'); - expect(report.details).not.toContain('CREATE TABLE `t10` (id int)'); - expect(report.details).toContain('...and 15 more changes'); + 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', () => { diff --git a/packages/cli/src/commands/migrate/migration-report.ts b/packages/cli/src/commands/migrate/migration-report.ts index 7edd17d2b6b..4fb6db3e294 100644 --- a/packages/cli/src/commands/migrate/migration-report.ts +++ b/packages/cli/src/commands/migrate/migration-report.ts @@ -1,4 +1,4 @@ -import { describeDiagnostic, MigrationDiagnostic } from '@vendure/core'; +import { MigrationDiagnostic } from '@vendure/core'; export interface MigrationReport { /** A one-line summary, suitable for a spinner. */ @@ -21,14 +21,14 @@ export function buildMigrationReport( ): 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.type === 'no-migrations-matched'); + 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(diagnostic => describeDiagnostic(diagnostic).join('\n')).join('\n\n'); + const details = diagnostics.map(render).join('\n\n'); return { summary, details, @@ -36,3 +36,15 @@ export function buildMigrationReport( 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/core/src/index.ts b/packages/core/src/index.ts index bb5236375a3..d31e6bda952 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,13 +18,8 @@ export * from './event-bus/index'; export * from './health-check/index'; export * from './i18n/index'; export * from './job-queue/index'; -export { describeDiagnostic, generateMigration, revertLastMigration, runMigrations } from './migrate'; -export type { - MigrationDiagnostic, - NoMigrationsMatchedDiagnostic, - RunMigrationsOptions, - SchemaOutOfSyncDiagnostic, -} from './migrate'; +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/migrate.ts b/packages/core/src/migrate.ts index c589ef9d860..6e2920d88d5 100644 --- a/packages/core/src/migrate.ts +++ b/packages/core/src/migrate.ts @@ -9,6 +9,7 @@ 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 @@ -32,60 +33,37 @@ export interface MigrationOptions { /** * @description - * 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. + * A condition detected while running migrations which cannot be inferred from the return value + * of {@link runMigrations}. * * @docsCategory migration - * @since 3.7.2 + * @since 3.7.4 */ -export interface NoMigrationsMatchedDiagnostic { - type: 'no-migrations-matched'; - /** - * @description - * The glob patterns configured in `dbConnectionOptions.migrations`. - */ - patterns: string[]; +export interface MigrationDiagnostic { /** * @description - * The directory the patterns were resolved against. + * 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. */ - cwd: string; -} - -/** - * @description - * The database schema does not match the current entity configuration, so a new migration - * needs to be generated. - * - * @docsCategory migration - * @since 3.7.2 - */ -export interface SchemaOutOfSyncDiagnostic { - type: 'schema-out-of-sync'; + code: 'no-migrations-matched' | 'schema-out-of-sync'; /** * @description - * The SQL statements which would bring the schema back into line with the configuration. + * The condition rendered as lines of human-readable text. */ - queries: string[]; + lines: string[]; } -/** - * @description - * A condition detected while running migrations which cannot be inferred from the return value - * of {@link runMigrations}. - * - * @docsCategory migration - * @since 3.7.2 - */ -export type MigrationDiagnostic = NoMigrationsMatchedDiagnostic | SchemaOutOfSyncDiagnostic; - /** * @description * Options for {@link runMigrations}. * * @docsCategory migration - * @since 3.7.2 + * @since 3.7.4 */ export interface RunMigrationsOptions { /** @@ -114,7 +92,7 @@ export async function runMigrations( const migrationsRan: string[] = []; const report = (diagnostic: MigrationDiagnostic) => { options?.onDiagnostic?.(diagnostic); - log(pc.yellow(describeDiagnostic(diagnostic).join('\n'))); + log(pc.yellow(diagnostic.lines.join('\n'))); }; try { const unmatched = await detectUnmatchedPatterns(connection); @@ -156,66 +134,35 @@ async function checkMigrationStatus(connection: Connection, report: (d: Migratio // `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 hasAnyEntityTable(connection))) { + if (!(await hasBeenSetUp(connection))) { return; } const builderLog = await connection.driver.createSchemaBuilder().log(); if (builderLog.upQueries.length) { - report({ type: 'schema-out-of-sync', queries: builderLog.upQueries.map(q => q.query) }); + report({ + code: 'schema-out-of-sync', + lines: [ + 'Your database schema does not match your current configuration. Generate a new migration for the following changes:', + ...builderLog.upQueries.map(q => ' - ' + q.query), + ], + }); } } -async function hasAnyEntityTable(connection: Connection): Promise { +/** + * 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 { - const tables = await queryRunner.getTables(connection.entityMetadatas.map(m => m.tablePath)); - return tables.length > 0; + return await queryRunner.hasTable(connection.getMetadata(Product).tablePath); } finally { await queryRunner.release(); } } -/** - * The full drift list can be the entire schema, which is more than a terminal should be asked - * to render. The queries themselves remain on the diagnostic for programmatic consumers. - */ -const maxDescribedQueries = 10; - -/** - * @description - * Renders a {@link MigrationDiagnostic} as lines of human-readable text. - * - * @docsCategory migration - * @since 3.7.2 - */ -export function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] { - switch (diagnostic.type) { - case 'no-migrations-matched': { - // 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 = diagnostic.patterns.some(pattern => !path.isAbsolute(pattern)); - return [ - '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:', - ...diagnostic.patterns.map(pattern => ' - ' + pattern), - ...(anyRelative - ? [`Relative patterns are resolved against the current directory (${diagnostic.cwd}).`] - : []), - ]; - } - case 'schema-out-of-sync': { - const shown = diagnostic.queries.slice(0, maxDescribedQueries); - const remaining = diagnostic.queries.length - shown.length; - return [ - 'Your database schema does not match your current configuration. Generate a new migration for the following changes:', - ...shown.map(query => ' - ' + query), - ...(remaining ? [` ...and ${remaining} more change${remaining === 1 ? '' : 's'}`] : []), - ]; - } - } -} - /** * 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 @@ -227,9 +174,9 @@ export function describeDiagnostic(diagnostic: MigrationDiagnostic): string[] { * 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 { +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; } @@ -243,7 +190,21 @@ async function detectUnmatchedPatterns( if (!executed.length) { return; } - return { type: 'no-migrations-matched', patterns, cwd: process.cwd() }; + // 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[] { From b60f7e94a16878eb782f550220733b25ea21a239 Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Thu, 10 Sep 2026 17:44:13 +0500 Subject: [PATCH 65/70] fix(dashboard): restore display component precedence over core cell fn (#5339) Co-authored-by: Michael Bromley --- .../customizing-list-pages.mdx | 2 + .../data-table/use-generated-columns.spec.tsx | 137 ++++++++++++++++++ .../data-table/use-generated-columns.tsx | 34 ++++- 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 packages/dashboard/src/lib/components/data-table/use-generated-columns.spec.tsx 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/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 }; } From 1a1570d72974bca06ad9e653d95004403e6ffebe Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:44:54 +0000 Subject: [PATCH 66/70] docs: Regenerate manifest.json --- docs/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 921710673e1..2b3b0dfb39e 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", From d985d32a693b2a75fec93fe22ae77d0b0af589cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:55:37 +0200 Subject: [PATCH 67/70] chore(deps): bump lucide-react from 0.475.0 to 0.577.0 (#5345) --- bun.lock | 4 ++-- packages/dashboard/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 4729f7dc47c..ec08d8f9709 100644 --- a/bun.lock +++ b/bun.lock @@ -383,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", @@ -4235,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=="], 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", From 2fd22d3dde816eadc506d3cfa18d2c3088bba8df Mon Sep 17 00:00:00 2001 From: "vendure-ci-automation-bot[bot]" <221402278+vendure-ci-automation-bot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:39:37 +0000 Subject: [PATCH 68/70] docs: Regenerate manifest.json --- docs/manifest.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 0feccda75ea..f595b00c330 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1068,7 +1068,7 @@ "title": "NativeAuthenticationStrategy", "slug": "native-authentication-strategy", "file": "docs/reference/typescript-api/auth/native-authentication-strategy.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "NoopSessionCacheStrategy", @@ -2290,7 +2290,7 @@ "title": "GenerateMigration", "slug": "generate-migration", "file": "docs/reference/typescript-api/migration/generate-migration.mdx", - "lastModified": "2026-09-01T08:40:40Z" + "lastModified": "2026-09-10T12:56:42+02:00" }, { "title": "MigrateAssetTranslationData", @@ -2308,13 +2308,13 @@ "title": "MigrationDiagnostic", "slug": "migration-diagnostic", "file": "docs/reference/typescript-api/migration/migration-diagnostic.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "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", @@ -2326,19 +2326,19 @@ "title": "RevertLastMigration", "slug": "revert-last-migration", "file": "docs/reference/typescript-api/migration/revert-last-migration.mdx", - "lastModified": "2026-09-01T08:40:40Z" + "lastModified": "2026-09-10T12:56:42+02:00" }, { "title": "RunMigrations", "slug": "run-migrations", "file": "docs/reference/typescript-api/migration/run-migrations.mdx", - "lastModified": "2026-09-01T08:40:40Z" + "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-01T08:40:40Z" + "lastModified": "2026-09-10T12:56:42+02:00" } ], "lastModified": "2026-01-28T14:49:21+01:00" @@ -2784,7 +2784,7 @@ "title": "RequestContext", "slug": "request-context", "file": "docs/reference/typescript-api/request/request-context.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "RequestContextService", @@ -2910,7 +2910,7 @@ "title": "AdministratorService", "slug": "administrator-service", "file": "docs/reference/typescript-api/services/administrator-service.mdx", - "lastModified": "2026-08-25T12:28:07Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "AssetService", @@ -2958,7 +2958,7 @@ "title": "CustomerService", "slug": "customer-service", "file": "docs/reference/typescript-api/services/customer-service.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "EntitySlugService", @@ -2982,7 +2982,7 @@ "title": "FulfillmentService", "slug": "fulfillment-service", "file": "docs/reference/typescript-api/services/fulfillment-service.mdx", - "lastModified": "2026-08-25T12:28:07Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "GlobalSettingsService", @@ -2994,7 +2994,7 @@ "title": "HistoryService", "slug": "history-service", "file": "docs/reference/typescript-api/services/history-service.mdx", - "lastModified": "2026-08-25T12:28:07Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "InitializerService", @@ -3024,7 +3024,7 @@ "title": "PaymentService", "slug": "payment-service", "file": "docs/reference/typescript-api/services/payment-service.mdx", - "lastModified": "2026-08-25T12:28:07Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "ProductOptionGroupService", @@ -3048,7 +3048,7 @@ "title": "ProductVariantService", "slug": "product-variant-service", "file": "docs/reference/typescript-api/services/product-variant-service.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "PromotionService", @@ -3066,7 +3066,7 @@ "title": "RoleService", "slug": "role-service", "file": "docs/reference/typescript-api/services/role-service.mdx", - "lastModified": "2026-08-25T12:28:07Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "SearchService", @@ -3144,7 +3144,7 @@ "title": "UserService", "slug": "user-service", "file": "docs/reference/typescript-api/services/user-service.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "ZoneService", @@ -3816,7 +3816,7 @@ "title": "Mutations", "slug": "mutations", "file": "docs/reference/graphql-api/shop/mutations.mdx", - "lastModified": "2026-08-25T09:10:29Z" + "lastModified": "2026-08-25T13:57:08Z" }, { "title": "Queries", From 9351c942cfcbaf9d7469d404413eef0e21cd9be4 Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Thu, 10 Sep 2026 18:45:37 +0500 Subject: [PATCH 69/70] fix(dashboard): refresh eligible shipping methods after draft order changes Fixes #5253 --- .../dashboard/e2e/tests/sales/orders.spec.ts | 142 +++++++++++++++++- .../_orders/orders_.draft.$id.tsx | 62 +++++--- 2 files changed, 175 insertions(+), 29 deletions(-) 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/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 ? ( - From b6b292372abfd74deda715af49879436f79b8e77 Mon Sep 17 00:00:00 2001 From: Ihor Barmak Date: Thu, 10 Sep 2026 17:20:48 +0300 Subject: [PATCH 70/70] perf(core): batch stockLevels lookups per request The Admin API `ProductVariant.stockLevels` field resolves one channel-filtered query per variant, so listing variants issues one query per row. #5224 batched the sibling path behind `getAvailableStock` but left this one alone. `getStockLevelsForVariant` now reads through a per-request DataLoader held on the RequestContext. The batch function runs the same joins and channel filter with `productVariantId IN (...)` and groups the rows by variant, so the resolver output is unchanged. The loader is keyed to the ctx that supplies the channel filter, so one loader per ctx cannot mix channels. `cache: false` for the same reason as #5224: batch, but do not memoize, so a write earlier in the request is not masked by a value read before it. Measured on the core e2e fixture, an Admin API page of 34 variants selecting stockLevels: 34 stock_level queries before, 1 after. --- .../services/stock-level.service.spec.ts | 130 +++++++++++++++++- .../service/services/stock-level.service.ts | 57 ++++++-- 2 files changed, 174 insertions(+), 13 deletions(-) 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);