From 3ffda0a29e7af83984fbfe104b0824c97ebfc23a Mon Sep 17 00:00:00 2001 From: martmull Date: Mon, 4 May 2026 15:55:31 +0200 Subject: [PATCH] Add twenty version validation (#20227) as title, server version is checked before app deploy, and app install commands ### New section in publishing doc image --------- Co-authored-by: Claude Opus 4.6 (1M context) --- package.json | 1 + .../developers/extend/apps/publishing.mdx | 33 ++++++++++ .../application-exception-filter.ts | 3 + .../application-install.service.ts | 36 +++++++++++ .../application-package.module.ts | 11 +++- .../application-version-validation.service.ts | 60 +++++++++++++++++++ ...plication-registration-exception-filter.ts | 3 + .../application-registration.exception.ts | 9 +++ .../application-registration.module.ts | 2 + .../application-tarball.service.ts | 34 +++++++++++ .../application/application.exception.ts | 9 +++ ...t-metadata-related-entity-ids.util.spec.ts | 4 +- ...t-metadata-related-entity-ids.util.spec.ts | 18 +++--- yarn.lock | 3 +- 14 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/application/application-package/application-version-validation.service.ts diff --git a/package.json b/package.json index f0159cdba9..e679aa7a04 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "concurrently": "^8.2.2", "http-server": "^14.1.1", "nx": "22.5.4", + "tsx": "^4.17.0", "verdaccio": "^6.3.1" }, "engines": { diff --git a/packages/twenty-docs/developers/extend/apps/publishing.mdx b/packages/twenty-docs/developers/extend/apps/publishing.mdx index 475f15d468..3077b5d0f9 100644 --- a/packages/twenty-docs/developers/extend/apps/publishing.mdx +++ b/packages/twenty-docs/developers/extend/apps/publishing.mdx @@ -77,6 +77,39 @@ Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allo {/* TODO: add screenshot of the Upgrade button */} +### Server version compatibility + +If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`: + +```json filename="package.json" +{ + "name": "twenty-my-app", + "version": "1.0.0", + "engines": { + "node": "^24.5.0", + "twenty": ">=2.3.0" + } +} +``` + +The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns: + +| Range | Meaning | +|-------|---------| +| `>=2.3.0` | Any server from 2.3.0 onward | +| `>=2.3.0 <3.0.0` | 2.3.0 or later, but below the next major | +| `^2.3.0` | Same as `>=2.3.0 <3.0.0` | + +**What happens at deploy and install time:** + +- If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version. +- If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps). +- If the server has no `APP_VERSION` configured, the check is skipped. + + +The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility. + + ## Automated CI/CD (scaffolded workflows) Apps generated with `create-twenty-app` ship with two GitHub Actions workflows out of the box, under `.github/workflows/`. They are ready to run as soon as you push the repo to GitHub — no extra setup is needed for CI, and CD only requires a single secret. diff --git a/packages/twenty-server/src/engine/core-modules/application/application-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/application/application-exception-filter.ts index 12ffc47a7d..f0d555223d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-exception-filter.ts @@ -31,12 +31,15 @@ export class ApplicationExceptionFilter implements ExceptionFilter { case ApplicationExceptionCode.SOURCE_CHANNEL_MISMATCH: case ApplicationExceptionCode.APP_ALREADY_INSTALLED: case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION: + case ApplicationExceptionCode.SERVER_VERSION_INCOMPATIBLE: + case ApplicationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT: throw new UserInputError(exception); case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED: case ApplicationExceptionCode.POST_INSTALL_ERROR: case ApplicationExceptionCode.PRE_INSTALL_ERROR: case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED: case ApplicationExceptionCode.UPGRADE_FAILED: + case ApplicationExceptionCode.INVALID_SERVER_VERSION: throw new InternalServerError(exception); default: { assertUnreachable(exception.code); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts index 55ada635fe..7ea3cfd97d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-install/application-install.service.ts @@ -21,6 +21,10 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationPackageFetcherService } from 'src/engine/core-modules/application/application-package/application-package-fetcher.service'; +import { + ApplicationVersionValidationService, + type VersionValidationFailureReason, +} from 'src/engine/core-modules/application/application-package/application-version-validation.service'; import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service'; import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; @@ -39,11 +43,22 @@ import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-func export class ApplicationInstallService { private readonly logger = new Logger(ApplicationInstallService.name); + private static readonly VERSION_REASON_TO_EXCEPTION_CODE: Record< + VersionValidationFailureReason, + ApplicationExceptionCode + > = { + INVALID_REQUIRED_VERSION: + ApplicationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT, + INVALID_SERVER_VERSION: ApplicationExceptionCode.INVALID_SERVER_VERSION, + INCOMPATIBLE: ApplicationExceptionCode.SERVER_VERSION_INCOMPATIBLE, + }; + constructor( @InjectRepository(ApplicationRegistrationEntity) private readonly appRegistrationRepository: Repository, private readonly applicationService: ApplicationService, private readonly applicationPackageFetcherService: ApplicationPackageFetcherService, + private readonly applicationVersionValidationService: ApplicationVersionValidationService, private readonly applicationSyncService: ApplicationSyncService, private readonly fileStorageService: FileStorageService, private readonly logicFunctionExecutorService: LogicFunctionExecutorService, @@ -118,6 +133,27 @@ export class ApplicationInstallService { return true; } + const requiredServerVersion = + resolvedPackage.packageJson.engines?.['twenty']; + + const versionValidation = + this.applicationVersionValidationService.validateServerCompatibility( + requiredServerVersion, + ); + + if (!versionValidation.compatible) { + await this.applicationPackageFetcherService.cleanupExtractedDir( + resolvedPackage.cleanupDir, + ); + + throw new ApplicationException( + versionValidation.message, + ApplicationInstallService.VERSION_REASON_TO_EXCEPTION_CODE[ + versionValidation.reason + ], + ); + } + const universalIdentifier = appRegistration.universalIdentifier; const existingApplication = diff --git a/packages/twenty-server/src/engine/core-modules/application/application-package/application-package.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-package/application-package.module.ts index 2f7c6239fb..90cf2241f3 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-package/application-package.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-package/application-package.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationPackageFetcherService } from 'src/engine/core-modules/application/application-package/application-package-fetcher.service'; +import { ApplicationVersionValidationService } from 'src/engine/core-modules/application/application-package/application-version-validation.service'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; @@ -13,7 +14,13 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty TwentyConfigModule, TypeOrmModule.forFeature([FileEntity, ApplicationEntity]), ], - providers: [ApplicationPackageFetcherService], - exports: [ApplicationPackageFetcherService], + providers: [ + ApplicationPackageFetcherService, + ApplicationVersionValidationService, + ], + exports: [ + ApplicationPackageFetcherService, + ApplicationVersionValidationService, + ], }) export class ApplicationPackageModule {} diff --git a/packages/twenty-server/src/engine/core-modules/application/application-package/application-version-validation.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-package/application-version-validation.service.ts new file mode 100644 index 0000000000..f144bce3b8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application-package/application-version-validation.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; + +import semver from 'semver'; +import { isDefined } from 'twenty-shared/utils'; + +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +export type VersionValidationFailureReason = + | 'INVALID_REQUIRED_VERSION' + | 'INVALID_SERVER_VERSION' + | 'INCOMPATIBLE'; + +export type VersionValidationResult = + | { compatible: true } + | { + compatible: false; + reason: VersionValidationFailureReason; + message: string; + }; + +@Injectable() +export class ApplicationVersionValidationService { + constructor(private readonly twentyConfigService: TwentyConfigService) {} + + validateServerCompatibility( + requiredServerVersion: string | undefined, + ): VersionValidationResult { + if (!isDefined(requiredServerVersion)) { + return { compatible: true }; + } + + if (!isDefined(semver.validRange(requiredServerVersion))) { + return { + compatible: false, + reason: 'INVALID_REQUIRED_VERSION', + message: `App manifest declares invalid engines.twenty value "${requiredServerVersion}". Must be a valid semver range.`, + }; + } + + const serverVersion = this.twentyConfigService.get('APP_VERSION'); + + if (!isDefined(serverVersion) || !isDefined(semver.valid(serverVersion))) { + return { + compatible: false, + reason: 'INVALID_SERVER_VERSION', + message: `Cannot verify server compatibility: APP_VERSION "${serverVersion ?? 'undefined'}" is not a valid semver version. Self-hosted instances must set a valid APP_VERSION.`, + }; + } + + if (!semver.satisfies(serverVersion, requiredServerVersion)) { + return { + compatible: false, + reason: 'INCOMPATIBLE', + message: `App requires Twenty server ${requiredServerVersion} but this server is ${serverVersion}.`, + }; + } + + return { compatible: true }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-exception-filter.ts index 41da4f8278..fb89d3bcb5 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration-exception-filter.ts @@ -22,6 +22,9 @@ export class ApplicationRegistrationExceptionFilter implements ExceptionFilter { case ApplicationRegistrationExceptionCode.INVALID_REDIRECT_URI: case ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH: case ApplicationRegistrationExceptionCode.UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED: + case ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE: + case ApplicationRegistrationExceptionCode.VERSION_ALREADY_EXISTS: + case ApplicationRegistrationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT: throw new UserInputError(exception); default: throw new InternalServerError(exception); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.exception.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.exception.ts index bbe24fdcaf..e7d7c7efdc 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.exception.ts @@ -13,6 +13,9 @@ export enum ApplicationRegistrationExceptionCode { SOURCE_CHANNEL_MISMATCH = 'SOURCE_CHANNEL_MISMATCH', VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND', VERSION_ALREADY_EXISTS = 'VERSION_ALREADY_EXISTS', + SERVER_VERSION_INCOMPATIBLE = 'SERVER_VERSION_INCOMPATIBLE', + INVALID_APP_ENGINE_REQUIREMENT = 'INVALID_APP_ENGINE_REQUIREMENT', + INVALID_SERVER_VERSION = 'INVALID_SERVER_VERSION', } const getExceptionUserFriendlyMessage = ( @@ -35,6 +38,12 @@ const getExceptionUserFriendlyMessage = ( return msg`Application registration variable not found.`; case ApplicationRegistrationExceptionCode.VERSION_ALREADY_EXISTS: return msg`This version is not higher than the currently deployed version. Please bump the version in package.json before deploying again.`; + case ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE: + return msg`This app requires a newer version of the Twenty server. Please upgrade your server or use a compatible app version.`; + case ApplicationRegistrationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT: + return msg`The app manifest declares an invalid server version requirement.`; + case ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION: + return msg`The server's APP_VERSION is not a valid semver version. Self-hosted instances must configure a valid APP_VERSION.`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts index ae2a7bc679..2d1837b753 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-registration.module.ts @@ -6,6 +6,7 @@ import { ApplicationRegistrationResolver } from 'src/engine/core-modules/applica import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; import { ApplicationRegistrationVariableModule } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.module'; import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service'; +import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; @@ -25,6 +26,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ]), ApplicationRegistrationVariableModule, ApplicationModule, + ApplicationPackageModule, DomainServerConfigModule, FeatureFlagModule, PermissionsModule, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-tarball.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-tarball.service.ts index a51b1d2ffd..5b0d21ce1d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/application-tarball.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/application-tarball.service.ts @@ -20,6 +20,10 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli import { extractTarballSecurely } from 'src/engine/core-modules/application/application-package/utils/extract-tarball-securely.util'; import { readJsonFile } from 'src/engine/core-modules/application/application-package/utils/read-json-file.util'; import { resolvePackageContentDir } from 'src/engine/core-modules/application/application-package/utils/tarball-utils'; +import { + ApplicationVersionValidationService, + type VersionValidationFailureReason, +} from 'src/engine/core-modules/application/application-package/application-version-validation.service'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import type { ApplicationManifest } from 'twenty-shared/application'; @@ -31,12 +35,25 @@ export const MAX_TARBALL_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; export class ApplicationTarballService { private readonly logger = new Logger(ApplicationTarballService.name); + private static readonly VERSION_REASON_TO_EXCEPTION_CODE: Record< + VersionValidationFailureReason, + ApplicationRegistrationExceptionCode + > = { + INVALID_REQUIRED_VERSION: + ApplicationRegistrationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT, + INVALID_SERVER_VERSION: + ApplicationRegistrationExceptionCode.INVALID_SERVER_VERSION, + INCOMPATIBLE: + ApplicationRegistrationExceptionCode.SERVER_VERSION_INCOMPATIBLE, + }; + constructor( @InjectRepository(ApplicationRegistrationEntity) private readonly appRegistrationRepository: Repository, private readonly fileStorageService: FileStorageService, private readonly applicationService: ApplicationService, private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService, + private readonly applicationVersionValidationService: ApplicationVersionValidationService, ) {} async uploadTarball(params: { @@ -66,6 +83,7 @@ export class ApplicationTarballService { const packageJson = await readJsonFile<{ version: string; + engines?: { twenty?: string }; }>(contentDir, 'package.json'); if (manifest === null) { @@ -75,6 +93,22 @@ export class ApplicationTarballService { ); } + const requiredServerVersion = packageJson?.engines?.twenty; + + const versionValidation = + this.applicationVersionValidationService.validateServerCompatibility( + requiredServerVersion, + ); + + if (!versionValidation.compatible) { + throw new ApplicationRegistrationException( + versionValidation.message, + ApplicationTarballService.VERSION_REASON_TO_EXCEPTION_CODE[ + versionValidation.reason + ], + ); + } + const universalIdentifier = params.universalIdentifier ?? manifest.application?.universalIdentifier; diff --git a/packages/twenty-server/src/engine/core-modules/application/application.exception.ts b/packages/twenty-server/src/engine/core-modules/application/application.exception.ts index f5d4cf52c8..ba124be317 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.exception.ts @@ -22,6 +22,9 @@ export enum ApplicationExceptionCode { POST_INSTALL_ERROR = 'POST_INSTALL_ERROR', APP_ALREADY_INSTALLED = 'APP_ALREADY_INSTALLED', CANNOT_DOWNGRADE_APPLICATION = 'CANNOT_DOWNGRADE_APPLICATION', + SERVER_VERSION_INCOMPATIBLE = 'SERVER_VERSION_INCOMPATIBLE', + INVALID_APP_ENGINE_REQUIREMENT = 'INVALID_APP_ENGINE_REQUIREMENT', + INVALID_SERVER_VERSION = 'INVALID_SERVER_VERSION', } const getApplicationExceptionUserFriendlyMessage = ( @@ -62,6 +65,12 @@ const getApplicationExceptionUserFriendlyMessage = ( return msg`This version of the application is already installed in this workspace.`; case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION: return msg`A higher version of this application is already installed. Downgrading is not allowed.`; + case ApplicationExceptionCode.SERVER_VERSION_INCOMPATIBLE: + return msg`This app requires a newer version of the Twenty server. Please upgrade your server or use a compatible app version.`; + case ApplicationExceptionCode.INVALID_APP_ENGINE_REQUIREMENT: + return msg`The app manifest declares an invalid server version requirement.`; + case ApplicationExceptionCode.INVALID_SERVER_VERSION: + return msg`The server's APP_VERSION is not a valid semver version. Self-hosted instances must configure a valid APP_VERSION.`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-object-metadata-related-entity-ids.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-object-metadata-related-entity-ids.util.spec.ts index 103e3773f4..867620bf40 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-object-metadata-related-entity-ids.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-object-metadata-related-entity-ids.util.spec.ts @@ -1,3 +1,5 @@ +import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util'; + let uuidCounter = 0; jest.mock('uuid', () => ({ @@ -6,8 +8,6 @@ jest.mock('uuid', () => ({ ), })); -import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util'; - describe('getStandardObjectMetadataRelatedEntityIds', () => { beforeEach(() => { uuidCounter = 0; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-page-layout-metadata-related-entity-ids.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-page-layout-metadata-related-entity-ids.util.spec.ts index ac40a48e85..5325317ea4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-page-layout-metadata-related-entity-ids.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/__tests__/get-standard-page-layout-metadata-related-entity-ids.util.spec.ts @@ -1,16 +1,20 @@ -let uuidCounter = 0; - -jest.mock('uuid', () => ({ - v4: jest.fn( - () => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`, - ), -})); +import { v4 } from 'uuid'; import { getStandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util'; +jest.mock('uuid', () => ({ + v4: jest.fn(), +})); + describe('getStandardPageLayoutMetadataRelatedEntityIds', () => { + let uuidCounter = 0; + beforeEach(() => { uuidCounter = 0; + (v4 as jest.Mock).mockImplementation( + () => + `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`, + ); }); afterAll(() => { diff --git a/yarn.lock b/yarn.lock index 4425fc16a0..a5c52481c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57593,7 +57593,7 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.19.3, tsx@npm:^4.7.0": +"tsx@npm:^4.17.0, tsx@npm:^4.19.3, tsx@npm:^4.7.0": version: 4.21.0 resolution: "tsx@npm:4.21.0" dependencies: @@ -58490,6 +58490,7 @@ __metadata: concurrently: "npm:^8.2.2" http-server: "npm:^14.1.1" nx: "npm:22.5.4" + tsx: "npm:^4.17.0" verdaccio: "npm:^6.3.1" languageName: unknown linkType: soft