diff --git a/.changeset/cli-posthog-build-telemetry.md b/.changeset/cli-posthog-build-telemetry.md new file mode 100644 index 0000000000..b1ce07674d --- /dev/null +++ b/.changeset/cli-posthog-build-telemetry.md @@ -0,0 +1,5 @@ +--- +"@tinacms/cli": patch +--- + +Track `tinacms build` invocations + outcomes in PostHog. Replaces the `metrics.tina.io` event for this command. Opt-out via `--noTelemetry` is unchanged. diff --git a/packages/@tinacms/cli/package.json b/packages/@tinacms/cli/package.json index 14ccc6a409..78d887b3c0 100644 --- a/packages/@tinacms/cli/package.json +++ b/packages/@tinacms/cli/package.json @@ -41,7 +41,8 @@ "@types/node": "^22.13.1", "@types/progress": "catalog:", "@types/prompts": "catalog:", - "jest": "catalog:" + "jest": "catalog:", + "ts-jest": "catalog:" }, "scripts": { "build": "tinacms-scripts build", @@ -91,6 +92,7 @@ "memory-level": "catalog:", "minimatch": "catalog:", "normalize-path": "catalog:", + "posthog-node": "^5.17.2", "prettier": "catalog:", "progress": "catalog:", "prompts": "catalog:", diff --git a/packages/@tinacms/cli/src/next/commands/build-command/index.ts b/packages/@tinacms/cli/src/next/commands/build-command/index.ts index 58afa881b1..237b66ae09 100644 --- a/packages/@tinacms/cli/src/next/commands/build-command/index.ts +++ b/packages/@tinacms/cli/src/next/commands/build-command/index.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import path from 'path'; import { ChangeType, diff } from '@graphql-inspector/core'; import { type Database, FilesystemBridge, buildSchema } from '@tinacms/graphql'; -import { Telemetry } from '@tinacms/metrics'; import { parseURL } from '@tinacms/schema-tools'; import { type SearchClient, @@ -31,6 +30,15 @@ import { BaseCommand } from '../baseCommands'; import { createDevServer } from '../dev-command/server'; import { buildProductionSpa } from './server'; import { waitForDB } from './waitForDB'; +import type { PostHog } from 'posthog-node'; +import { + BuildFinishedEvent, + BuildInvokeEvent, + BuildInvokeEventPayload, + generateSessionId, + initializePostHog, + postHogCapture, +} from '../../../utils/posthog'; export class BuildCommand extends BaseCommand { static paths = [['build']]; @@ -82,7 +90,19 @@ export class BuildCommand extends BaseCommand { description: `Build the CMS and autogenerated modules for usage with TinaCloud`, }); + private posthogClient: PostHog | null = null; + private buildStartedAt = 0; + private buildRunId = generateSessionId(); + async catch(error: any): Promise { + const errorCode = + (error as { errorCode?: string })?.errorCode ?? 'ERR_BUILD_FAILED'; + postHogCapture(this.posthogClient, this.buildRunId, BuildFinishedEvent, { + success: false, + durationMs: Date.now() - this.buildStartedAt, + errorCode, + }); + if (this.posthogClient) await this.posthogClient.shutdown(); console.error(error); process.exit(1); } @@ -124,6 +144,34 @@ export class BuildCommand extends BaseCommand { } const localContentOnly = this.contentOption === 'local'; + // Init telemetry + fire invoke BEFORE any work that can fail. From here + // on, failures throw and catch() fires a finished{success:false} event + // with the thrown error's errorCode. + this.posthogClient = this.noTelemetry + ? null + : await initializePostHog( + 'https://identity-v2.tinajs.io/v2/posthog-token', + false + ); + + const buildInvokeEventPayload: BuildInvokeEventPayload = { + hasLocalOption: Boolean(this.localOption), + hasContentLocal: localContentOnly, + skipIndexing: Boolean(this.skipIndexing), + partialReindex: Boolean(this.partialReindex), + hasPreviewName: Boolean(this.previewName), + specifiesTinaGraphQLVersions: Boolean(this.tinaGraphQLVersion), + skipCloudChecks: Boolean(this.skipCloudChecks), + skipSearchIndex: Boolean(this.skipSearchIndex), + }; + this.buildStartedAt = Date.now(); + postHogCapture( + this.posthogClient, + this.buildRunId, + BuildInvokeEvent, + buildInvokeEventPayload + ); + try { await configManager.processConfig(); } catch (e) { @@ -131,18 +179,11 @@ export class BuildCommand extends BaseCommand { logger.error( dangerText('Unable to build, please fix your Tina config and try again') ); - process.exit(1); + throw Object.assign(new Error(e.message), { + errorCode: 'ERR_CONFIG_LOAD_FAILED', + }); } - // Track localContentPath usage so we can measure adoption of the - // multi-repo separation. - const telemetry = new Telemetry({ disabled: this.noTelemetry }); - await telemetry.submitRecord({ - event: { - name: 'tinacms:cli:build:invoke', - hasLocalContentPath: Boolean(configManager.config.localContentPath), - }, - }); if (localContentOnly && !this.localOption) { const config = configManager.config; const missing = []; @@ -150,12 +191,11 @@ export class BuildCommand extends BaseCommand { if (!config.clientId) missing.push('clientId'); if (!config.token) missing.push('token'); if (missing.length > 0) { - logger.error( - `${dangerText( - `ERROR: --content=local requires ${missing.join(', ')} to be configured, since the generated client must point to TinaCloud.` - )}` - ); - process.exit(1); + const message = `--content=local requires ${missing.join(', ')} to be configured, since the generated client must point to TinaCloud.`; + logger.error(`${dangerText(`ERROR: ${message}`)}`); + throw Object.assign(new Error(message), { + errorCode: 'ERR_MISSING_CLOUD_CREDS', + }); } } let server: ViteDevServer | undefined; @@ -210,7 +250,9 @@ export class BuildCommand extends BaseCommand { if (this.verbose) { console.error(e); } - process.exit(1); + throw Object.assign(new Error(e.message), { + errorCode: 'ERR_INDEXING_FAILED', + }); } } @@ -288,7 +330,9 @@ export class BuildCommand extends BaseCommand { if (this.verbose) { console.error(e); } - process.exit(1); + throw Object.assign(new Error(e.message), { + errorCode: 'ERR_CLOUD_CHECK_FAILED', + }); } } @@ -378,7 +422,9 @@ export class BuildCommand extends BaseCommand { }); if (err) { logger.error(`${dangerText(`ERROR: ${err.message}`)}`); - process.exit(1); + throw Object.assign(new Error(err.message), { + errorCode: 'ERR_SEARCH_INDEX_FAILED', + }); } } @@ -428,6 +474,12 @@ export class BuildCommand extends BaseCommand { process.env.NODE_ENV = 'production'; } } + postHogCapture(this.posthogClient, this.buildRunId, BuildFinishedEvent, { + success: true, + durationMs: Date.now() - this.buildStartedAt, + }); + if (this.posthogClient) await this.posthogClient.shutdown(); + if (this.subCommand) { await this.startSubCommand(); } else { diff --git a/packages/@tinacms/cli/src/utils/fetchPostHogConfig.test.ts b/packages/@tinacms/cli/src/utils/fetchPostHogConfig.test.ts new file mode 100644 index 0000000000..f0609e94bc --- /dev/null +++ b/packages/@tinacms/cli/src/utils/fetchPostHogConfig.test.ts @@ -0,0 +1,69 @@ +import fetchPostHogConfig from './fetchPostHogConfig'; + +describe('fetchPostHogConfig', () => { + const realFetch = globalThis.fetch; + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + globalThis.fetch = realFetch; + warnSpy.mockRestore(); + }); + + it('returns api_key + host on a 200 JSON response', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ api_key: 'phc_test', host: 'https://eu.example' }), + }) as unknown as typeof fetch; + + expect(await fetchPostHogConfig('https://x')).toEqual({ + POSTHOG_API_KEY: 'phc_test', + POSTHOG_ENDPOINT: 'https://eu.example', + }); + }); + + it('returns {} on a non-2xx response', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + statusText: 'Bad Gateway', + json: async () => ({}), + }) as unknown as typeof fetch; + + expect(await fetchPostHogConfig('https://x')).toEqual({}); + }); + + it('returns {} on a network error', async () => { + globalThis.fetch = jest + .fn() + .mockRejectedValue(new Error('ENOTFOUND')) as unknown as typeof fetch; + + expect(await fetchPostHogConfig('https://x')).toEqual({}); + }); + + it('returns {} when JSON parse fails', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => { + throw new SyntaxError('Unexpected token'); + }, + }) as unknown as typeof fetch; + + expect(await fetchPostHogConfig('https://x')).toEqual({}); + }); + + it('passes an AbortSignal to fetch so offline callers do not hang', async () => { + const fetchMock = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ api_key: 'k', host: 'h' }), + }); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + await fetchPostHogConfig('https://x'); + + const [, init] = fetchMock.mock.calls[0]; + expect(init.signal).toBeInstanceOf(AbortSignal); + }); +}); diff --git a/packages/@tinacms/cli/src/utils/fetchPostHogConfig.ts b/packages/@tinacms/cli/src/utils/fetchPostHogConfig.ts new file mode 100644 index 0000000000..f1f2a0f882 --- /dev/null +++ b/packages/@tinacms/cli/src/utils/fetchPostHogConfig.ts @@ -0,0 +1,33 @@ +export interface PostHogConfig { + POSTHOG_API_KEY?: string; + POSTHOG_ENDPOINT?: string; +} + +export default async function fetchPostHogConfig( + endpointUrl: string +): Promise { + try { + const response = await fetch(endpointUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + // Cap latency for offline / firewalled developers. Endpoint is + // typically single-digit ms when reachable; a timeout returns {} + // and disables telemetry for this run, which is the right behavior. + signal: AbortSignal.timeout(2000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch PostHog config: ${response.statusText}`); + } + + const config = await response.json(); + return { + POSTHOG_API_KEY: config.api_key, + POSTHOG_ENDPOINT: config.host, + }; + } catch { + return {}; + } +} diff --git a/packages/@tinacms/cli/src/utils/posthog.test.ts b/packages/@tinacms/cli/src/utils/posthog.test.ts new file mode 100644 index 0000000000..29ea7bb03d --- /dev/null +++ b/packages/@tinacms/cli/src/utils/posthog.test.ts @@ -0,0 +1,142 @@ +import { PostHog } from 'posthog-node'; +import { + generateSessionId, + initializePostHog, + postHogCapture, +} from './posthog'; + +describe('generateSessionId', () => { + it('returns a UUID-formatted string', () => { + const id = generateSessionId(); + expect(id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ); + }); + + it('returns a different value on each call', () => { + expect(generateSessionId()).not.toBe(generateSessionId()); + }); +}); + +describe('initializePostHog', () => { + const realFetch = globalThis.fetch; + const originalTinaDev = process.env.TINA_DEV; + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + globalThis.fetch = realFetch; + if (originalTinaDev === undefined) { + delete process.env.TINA_DEV; + } else { + process.env.TINA_DEV = originalTinaDev; + } + warnSpy.mockRestore(); + }); + + it('returns null when TINA_DEV=true and never hits the network', async () => { + process.env.TINA_DEV = 'true'; + const fetchMock = jest.fn(); + globalThis.fetch = fetchMock as unknown as typeof fetch; + + expect(await initializePostHog('https://x')).toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('returns null when no configEndpoint is provided', async () => { + delete process.env.TINA_DEV; + expect(await initializePostHog()).toBeNull(); + }); + + it('returns null when the config endpoint returns no api_key', async () => { + delete process.env.TINA_DEV; + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({}), + }) as unknown as typeof fetch; + + expect(await initializePostHog('https://x')).toBeNull(); + }); + + it('returns null on a network failure (graceful degradation)', async () => { + delete process.env.TINA_DEV; + globalThis.fetch = jest + .fn() + .mockRejectedValue(new Error('ENOTFOUND')) as unknown as typeof fetch; + + expect(await initializePostHog('https://x')).toBeNull(); + }); + + it('returns a PostHog instance when an api_key comes back', async () => { + delete process.env.TINA_DEV; + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + api_key: 'phc_test', + host: 'https://app.posthog.com', + }), + }) as unknown as typeof fetch; + + const client = await initializePostHog('https://x'); + expect(client).toBeInstanceOf(PostHog); + await client?.shutdown(); + }); +}); + +describe('postHogCapture', () => { + let errorSpy: jest.SpyInstance; + + beforeEach(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + errorSpy.mockRestore(); + }); + + it('no-ops when the client is null', () => { + expect(() => + postHogCapture(null as unknown as PostHog, 'distinct', 'evt', {}) + ).not.toThrow(); + }); + + it('calls client.capture with the given distinctId and event name', () => { + const capture = jest.fn(); + const client = { capture } as unknown as PostHog; + + postHogCapture(client, 'user-123', 'tinacms-cli-build-invoke', { + hasLocalOption: true, + }); + + expect(capture).toHaveBeenCalledTimes(1); + const arg = capture.mock.calls[0][0]; + expect(arg.distinctId).toBe('user-123'); + expect(arg.event).toBe('tinacms-cli-build-invoke'); + }); + + it('appends system: tinacms/cli to properties', () => { + const capture = jest.fn(); + const client = { capture } as unknown as PostHog; + + postHogCapture(client, 'd', 'e', { hasLocalOption: true }); + + const arg = capture.mock.calls[0][0]; + expect(arg.properties).toMatchObject({ + hasLocalOption: true, + system: 'tinacms/cli', + }); + }); + + it('swallows errors thrown by client.capture so telemetry never breaks the CLI', () => { + const capture = jest.fn().mockImplementation(() => { + throw new Error('boom'); + }); + const client = { capture } as unknown as PostHog; + + expect(() => postHogCapture(client, 'd', 'e', {})).not.toThrow(); + expect(errorSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/@tinacms/cli/src/utils/posthog.ts b/packages/@tinacms/cli/src/utils/posthog.ts new file mode 100644 index 0000000000..a38676ff87 --- /dev/null +++ b/packages/@tinacms/cli/src/utils/posthog.ts @@ -0,0 +1,76 @@ +import { randomUUID } from 'node:crypto'; +import { PostHog } from 'posthog-node'; +import fetchPostHogConfig from './fetchPostHogConfig'; + +export function generateSessionId(): string { + return randomUUID(); +} + +export const BuildInvokeEvent = 'tinacms-cli-build-invoke'; +export type BuildInvokeEventPayload = { + hasLocalOption: boolean; + hasContentLocal: boolean; + skipIndexing: boolean; + partialReindex: boolean; + hasPreviewName: boolean; + specifiesTinaGraphQLVersions?: boolean; + skipCloudChecks: boolean; + skipSearchIndex: boolean; +}; + +export const BuildFinishedEvent = 'tinacms-cli-build-finished'; +export type BuildFinishedEventPayload = { + success: boolean; + durationMs: number; + errorCode?: string; +}; + +export async function initializePostHog( + configEndpoint?: string, + disableGeoip?: boolean +): Promise { + // Skip the config fetch + client construction entirely when contributors + // are iterating on the CLI locally. Saves the network round-trip on every + // dev invocation. + if (process.env.TINA_DEV === 'true') return null; + + let apiKey: string | undefined; + let endpoint: string | undefined; + + if (configEndpoint) { + const config = await fetchPostHogConfig(configEndpoint); + apiKey = config.POSTHOG_API_KEY; + endpoint = config.POSTHOG_ENDPOINT; + } + + if (!apiKey) return null; + + return new PostHog(apiKey, { + host: endpoint, + disableGeoip: disableGeoip ?? true, + }); +} + +export function postHogCapture( + client: PostHog, + distinctId: string, + event: string, + properties: Record +): void { + if (!client) { + return; + } + + try { + client.capture({ + distinctId, + event, + properties: { + ...properties, + system: 'tinacms/cli', + }, + }); + } catch (error) { + console.error('Error capturing event:', error); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f04b6d3ff7..4e6880b2eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1272,13 +1272,13 @@ importers: version: 29.5.14 jest: specifier: 'catalog:' - version: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) + version: 29.7.0(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.3)) next: specifier: 14.2.35 version: 14.2.35(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.3) ts-jest: specifier: 'catalog:' - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -1423,6 +1423,9 @@ importers: normalize-path: specifier: 'catalog:' version: 3.0.0 + posthog-node: + specifier: ^5.17.2 + version: 5.17.3 prettier: specifier: 'catalog:' version: 2.8.8 @@ -1511,6 +1514,9 @@ importers: jest: specifier: 'catalog:' version: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) + ts-jest: + specifier: 'catalog:' + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.24.2)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) packages/@tinacms/datalayer: dependencies: @@ -8031,6 +8037,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -18991,41 +18998,6 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.17 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -23680,21 +23652,6 @@ snapshots: crc-32@1.2.2: {} - create-jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -24640,7 +24597,7 @@ snapshots: eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.32.0(eslint@7.32.0))(eslint@7.32.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@7.32.0) eslint-plugin-react: 7.37.5(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.2(eslint@7.32.0) @@ -24663,7 +24620,7 @@ snapshots: dependencies: debug: 4.4.3 eslint: 7.32.0 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.11 @@ -24682,7 +24639,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -26204,25 +26161,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest-cli@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) @@ -26280,37 +26218,6 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0(babel-plugin-macros@3.1.0) - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.17 - ts-node: 10.9.2(@types/node@22.19.17)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-config@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -26946,18 +26853,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) @@ -31306,12 +31201,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.24.2)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.17)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.3)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.19.3)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -31324,6 +31219,7 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.24.2 jest-util: 30.2.0 ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@25.1.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.3)))(typescript@5.9.3):