diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 356a2fc26..a1d7a09c5 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -133,10 +133,3 @@ jobs: CLICKHOUSE_CLOUD_PASSWORD: ${{ secrets.INTEGRATIONS_TEAM_TESTS_CLOUD_PASSWORD_SMT_PROD }} run: | npm run run-examples - - success: - needs: ["code-quality", "run-examples"] - runs-on: ubuntu-latest - steps: - - name: All examples checks passed - run: echo "All examples checks passed! 🎉" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 968207041..b71efe8d9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -341,6 +341,14 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Start ClickHouse (stable) in Docker + uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0 + env: + CLICKHOUSE_VERSION: latest + with: + compose-file: "docker-compose.yml" + down-flags: "--volumes" + - name: Setup NodeJS ${{ matrix.node }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: @@ -401,3 +409,9 @@ jobs: env: EXPECTED_VERSION: ${{ env.PUBLISHED_VERSION }} run: node src/index.ts + + # End-to-end integration against the installed package and a live + # single-node ClickHouse (started above): create/insert/select, stream, + # and confirm a bad query surfaces as a ClickHouseError. + - name: Run integration tests against the published package + run: node src/integration.ts diff --git a/.github/workflows/tests-dist.yml b/.github/workflows/tests-dist.yml new file mode 100644 index 000000000..49279e0cd --- /dev/null +++ b/.github/workflows/tests-dist.yml @@ -0,0 +1,112 @@ +name: "dist" + +permissions: {} +on: + workflow_dispatch: + push: + branches: + - main + - release + paths: + - "packages/**" + - "tests/**" + - "package.json" + - "package-lock.json" + - "tsconfig.base.json" + - "tsconfig.dev.json" + - "eslint.config.base.mjs" + - "docker-compose.yml" + - "vitest.node.config.ts" + - "vitest.node.setup.ts" + - "vitest.web.config.ts" + - "vitest.web.setup.ts" + - ".github/workflows/tests-dist.yml" + pull_request: + paths: + - "packages/**" + - "tests/**" + - "package.json" + - "package-lock.json" + - "tsconfig.base.json" + - "tsconfig.dev.json" + - "eslint.config.base.mjs" + - "docker-compose.yml" + - "vitest.node.config.ts" + - "vitest.node.setup.ts" + - "vitest.web.config.ts" + - "vitest.web.setup.ts" + - ".github/workflows/tests-dist.yml" + + schedule: + - cron: "0 9 * * *" + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +# Integration tests resolved against the BUILT `dist` packages (TEST_TARGET=dist) +# instead of the workspace `src`. This is the published-surface / bundle guard: +# it catches build output, packaging, and single-bundle module-identity issues +# (e.g. a value class like SettingsMap/TupleParam resolving to a second copy and +# breaking the client's internal `instanceof` checks) that the fast, build-free +# `src` loop cannot. Behavioural coverage across Node/ClickHouse versions already +# runs in the `node`/`web` workflows, so this targets only the stable ClickHouse +# release on a local single-node instance with the canonical Node.js version. +jobs: + node-dist-integration: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Start ClickHouse (stable) in Docker + uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0 + env: + CLICKHOUSE_VERSION: latest + with: + compose-file: "docker-compose.yml" + down-flags: "--volumes" + + - name: Setup NodeJS + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + # test:node:integration:dist builds the workspace packages first, then runs + # the node integration suite with TEST_TARGET=dist. + - name: Run node integration tests against dist + run: npm run test:node:integration:dist + + web-dist-integration: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Start ClickHouse (stable) in Docker + uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0 + env: + CLICKHOUSE_VERSION: latest + with: + compose-file: "docker-compose.yml" + down-flags: "--volumes" + + - name: Setup NodeJS + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24 + + - name: Install dependencies + run: | + npm ci + npx playwright install chromium + + # test:web:integration:dist builds the workspace packages first, then runs + # the web integration suite with TEST_TARGET=dist. + - name: Run web integration tests against dist + env: + BROWSER: chromium + run: npm run test:web:integration:dist diff --git a/.github/workflows/tests-node.yml b/.github/workflows/tests-node.yml index cfb1d2911..7a869b6d8 100644 --- a/.github/workflows/tests-node.yml +++ b/.github/workflows/tests-node.yml @@ -357,24 +357,3 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/lcov.info fail_ci_if_error: true - - success: - needs: - [ - "code-quality", - "common-unit-tests", - "unit-tests", - "integration-tests-local-single-node", - "integration-tests-local-cluster", - "integration-tests-cloud", - ] - # Run even if some needed jobs were skipped (e.g. cloud jobs on PRs from forks), - # but still fail if any of them failed or were cancelled. - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Fail if any needed job failed or was cancelled - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} - run: exit 1 - - name: All tests passed - run: echo "All tests passed! 🎉" diff --git a/.github/workflows/tests-oss-dependents.yml b/.github/workflows/tests-oss-dependents.yml index 14368c0c0..eedcbd315 100644 --- a/.github/workflows/tests-oss-dependents.yml +++ b/.github/workflows/tests-oss-dependents.yml @@ -87,14 +87,3 @@ jobs: - name: Run OSS-dependent integration tests run: | npm run test:node:oss-dependents - - success: - needs: ["oss-dependents-tests"] - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Fail if any needed job failed or was cancelled - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} - run: exit 1 - - name: All OSS-dependent tests passed - run: echo "All OSS-dependent tests passed! 🎉" diff --git a/.github/workflows/tests-web.yml b/.github/workflows/tests-web.yml index 129d93de9..b935ffa78 100644 --- a/.github/workflows/tests-web.yml +++ b/.github/workflows/tests-web.yml @@ -255,22 +255,3 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/lcov.info fail_ci_if_error: true - - success: - needs: - [ - "common-unit-tests", - "all-tests-local-single-node", - "integration-tests-local-cluster", - "integration-tests-cloud", - ] - # Run even if some needed jobs were skipped (e.g. cloud jobs on PRs from forks), - # but still fail if any of them failed or were cancelled. - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Fail if any needed job failed or was cancelled - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} - run: exit 1 - - name: All tests passed - run: echo "All tests passed! 🎉" diff --git a/package.json b/package.json index 0001af256..764d19367 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test:node:unit:bun": "CLICKHOUSE_TEST_SKIP_INIT=1 TEST_MODE=unit bun --bun vitest -c vitest.node.config.ts", "test:node:integration:tls": "TEST_MODE=tls vitest -c vitest.node.config.ts", "test:node:integration": "TEST_MODE=integration vitest -c vitest.node.config.ts", + "test:node:integration:dist": "npm run build && TEST_TARGET=dist TEST_MODE=integration vitest -c vitest.node.config.ts", "test:node:oss-dependents": "TEST_MODE=oss-dependents vitest -c vitest.node.config.ts", "test:node:integration:local_cluster": "CLICKHOUSE_TEST_ENVIRONMENT=local_cluster TEST_MODE=integration vitest -c vitest.node.config.ts", "test:node:integration:cloud": "CLICKHOUSE_TEST_ENVIRONMENT=cloud TEST_MODE=integration vitest -c vitest.node.config.ts", @@ -40,6 +41,7 @@ "test:node:coverage": "VITEST_COVERAGE=true TEST_MODE=all vitest -c vitest.node.config.ts", "test:web:unit": "CLICKHOUSE_TEST_SKIP_INIT=1 TEST_MODE=unit vitest -c vitest.web.config.ts", "test:web:integration": "TEST_MODE=integration vitest -c vitest.web.config.ts", + "test:web:integration:dist": "npm run build && TEST_TARGET=dist TEST_MODE=integration vitest -c vitest.web.config.ts", "test:web:integration:local_cluster": "TEST_MODE=integration CLICKHOUSE_TEST_ENVIRONMENT=local_cluster vitest -c vitest.web.config.ts", "test:web:integration:cloud": "TEST_MODE=integration CLICKHOUSE_TEST_ENVIRONMENT=cloud vitest -c vitest.web.config.ts", "test:web:integration:cloud:jwt": "TEST_MODE=jwt CLICKHOUSE_TEST_ENVIRONMENT=cloud vitest -c vitest.web.config.ts", diff --git a/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts b/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts index b482da565..918d7974e 100644 --- a/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts +++ b/packages/client-node/__tests__/integration/node_custom_http_agent.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { TestEnv, isOnEnv } from "@test/utils/test_env"; import http from "http"; import Http from "http"; -import { createClient } from "../../src"; +import { createClient } from "@clickhouse/client"; /** HTTPS agent tests are in tls.test.ts as it requires a secure connection. */ diff --git a/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts b/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts index 507352407..8af0fd26a 100644 --- a/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts +++ b/packages/client-node/__tests__/integration/node_eager_socket_destroy.test.ts @@ -8,7 +8,7 @@ import { import { createTestClient } from "@test/utils/client"; import * as http from "http"; import { AddressInfo } from "net"; -import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client"; describe("[Node.js] Eager socket destruction", () => { afterEach(() => { @@ -52,7 +52,7 @@ describe("[Node.js] Eager socket destruction", () => { LoggerClass: CapturingLogger, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); try { // Capture the current timestamp before the first request so that @@ -130,7 +130,7 @@ describe("[Node.js] Eager socket destruction", () => { LoggerClass: CapturingLogger, level: ClickHouseLogLevel.WARN, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); try { // Capture the current timestamp before the first request so that diff --git a/packages/client-node/__tests__/integration/node_errors_parsing.test.ts b/packages/client-node/__tests__/integration/node_errors_parsing.test.ts index 2ee1f3958..0ffe7a1c6 100644 --- a/packages/client-node/__tests__/integration/node_errors_parsing.test.ts +++ b/packages/client-node/__tests__/integration/node_errors_parsing.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { createClient } from "../../src"; +import { createClient } from "@clickhouse/client"; describe("[Node.js] errors parsing", () => { it("should return an error when URL is unreachable", async () => { diff --git a/packages/client-node/__tests__/integration/node_exec.test.ts b/packages/client-node/__tests__/integration/node_exec.test.ts index 43507d4df..b6454f23e 100644 --- a/packages/client-node/__tests__/integration/node_exec.test.ts +++ b/packages/client-node/__tests__/integration/node_exec.test.ts @@ -1,29 +1,17 @@ -import { - DefaultLogger, - LogWriter, - type ClickHouseClient, - ClickHouseLogLevel, -} from "@clickhouse/client-common"; +import type { ClickHouseClient } from "@clickhouse/client-common"; import { describe, it, beforeEach, afterEach, expect } from "vitest"; import { createSimpleTable } from "@test/fixtures/simple_table"; import { createTestClient } from "@test/utils/client"; import { guid } from "@test/utils/guid"; import Stream from "stream"; import Zlib from "zlib"; -import { ResultSet } from "../../src"; -import { drainStreamInternal } from "../../src/connection/stream"; -import { getAsText } from "../../src/utils"; +import { ResultSet, drainStream } from "@clickhouse/client"; +import { getAsText } from "../utils/stream"; describe("[Node.js] exec", () => { let client: ClickHouseClient; - let log_writer: LogWriter; beforeEach(() => { client = createTestClient(); - log_writer = new LogWriter( - new DefaultLogger(), - "Connection", - ClickHouseLogLevel.OFF, - ); }); afterEach(async () => { await client.close(); @@ -99,15 +87,7 @@ describe("[Node.js] exec", () => { values: stream, }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket - await drainStreamInternal( - { - op: "Insert", - query_id: execResult.query_id, - log_writer, - log_level: ClickHouseLogLevel.OFF, - }, - execResult.stream, - ); + await drainStream(execResult.stream); await checkInsertedValues([ { id: "42", @@ -132,15 +112,7 @@ describe("[Node.js] exec", () => { stream.push(null); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket const execResult = await execPromise; - await drainStreamInternal( - { - op: "Insert", - query_id: execResult.query_id, - log_writer, - log_level: ClickHouseLogLevel.OFF, - }, - execResult.stream, - ); + await drainStream(execResult.stream); await checkInsertedValues([]); }); @@ -159,15 +131,7 @@ describe("[Node.js] exec", () => { values: stream, }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket - await drainStreamInternal( - { - op: "Insert", - query_id: execResult.query_id, - log_writer, - log_level: ClickHouseLogLevel.OFF, - }, - execResult.stream, - ); + await drainStream(execResult.stream); await checkInsertedValues([ { id: "42", @@ -191,15 +155,7 @@ describe("[Node.js] exec", () => { values: stream, }); // the result stream contains nothing useful for an insert and should be immediately drained to release the socket - await drainStreamInternal( - { - op: "Insert", - query_id: execResult.query_id, - log_writer, - log_level: ClickHouseLogLevel.OFF, - }, - execResult.stream, - ); + await drainStream(execResult.stream); await checkInsertedValues([]); }); diff --git a/packages/client-node/__tests__/integration/node_jwt_auth.test.ts b/packages/client-node/__tests__/integration/node_jwt_auth.test.ts index 51d137259..27f790072 100644 --- a/packages/client-node/__tests__/integration/node_jwt_auth.test.ts +++ b/packages/client-node/__tests__/integration/node_jwt_auth.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { TestEnv, isOnEnv } from "@test/utils/test_env"; import { EnvKeys, getFromEnv, maybeGetFromEnv } from "@test/utils/env"; -import { createClient } from "../../src"; -import type { NodeClickHouseClient } from "../../src/client"; +import { createClient } from "@clickhouse/client"; +import type { ClickHouseClient } from "@clickhouse/client"; describe.skipIf(!isOnEnv(TestEnv.Cloud))("[Node.js] JWT auth", () => { - let jwtClient: NodeClickHouseClient; + let jwtClient: ClickHouseClient; let url: string; let jwt: string | undefined; diff --git a/packages/client-node/__tests__/integration/node_keep_alive.test.ts b/packages/client-node/__tests__/integration/node_keep_alive.test.ts index 7476f07cf..f933a5f5d 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive.test.ts @@ -3,8 +3,8 @@ import { ClickHouseLogLevel } from "@clickhouse/client-common"; import { createSimpleTable } from "@test/fixtures/simple_table"; import { guid } from "@test/utils/guid"; import { sleep } from "@test/utils/sleep"; -import type { ClickHouseClient } from "../../src"; -import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import type { ClickHouseClient } from "@clickhouse/client"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client"; import { createNodeTestClient } from "../utils/node_client"; describe("[Node.js] Keep Alive", () => { @@ -22,7 +22,7 @@ describe("[Node.js] Keep Alive", () => { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); expect(await query(0)).toEqual(1); await sleep(socketTTL); // this one could've failed without idle socket release @@ -35,7 +35,7 @@ describe("[Node.js] Keep Alive", () => { keep_alive: { enabled: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); expect(await query(0)).toEqual(1); await sleep(socketTTL); // this one won't fail cause a new socket will be assigned @@ -48,7 +48,7 @@ describe("[Node.js] Keep Alive", () => { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); const results = await Promise.all( [...Array(4).keys()].map((n) => query(n)), @@ -85,7 +85,7 @@ describe("[Node.js] Keep Alive", () => { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); tableName = `keep_alive_single_connection_insert_${guid()}`; await createSimpleTable(client, tableName); await insert(0); @@ -109,7 +109,7 @@ describe("[Node.js] Keep Alive", () => { enabled: true, idle_socket_ttl: socketTTL, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); tableName = `keep_alive_multiple_connection_insert_${guid()}`; await createSimpleTable(client, tableName); await Promise.all([...Array(3).keys()].map((n) => insert(n))); diff --git a/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts b/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts index 03a361df9..7b31d0f73 100644 --- a/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts +++ b/packages/client-node/__tests__/integration/node_keep_alive_header.test.ts @@ -2,7 +2,7 @@ import { ClickHouseLogLevel, Logger } from "@clickhouse/client-common"; import { describe, it } from "vitest"; import { createTestClient } from "@test/utils/client"; import net from "net"; -import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client"; import { AddressInfo } from "net"; describe.concurrent("Handling keep-alive header", () => { @@ -50,7 +50,7 @@ describe.concurrent("Handling keep-alive header", () => { LoggerClass, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); expect(await client.ping({ select: true })).toMatchObject({ success: true, @@ -142,7 +142,7 @@ describe.concurrent("Handling keep-alive header", () => { LoggerClass, level: ClickHouseLogLevel.TRACE, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); expect(await client.ping({ select: true })).toMatchObject({ success: true, diff --git a/packages/client-node/__tests__/integration/node_max_open_connections.test.ts b/packages/client-node/__tests__/integration/node_max_open_connections.test.ts index 9749f3d81..5c4a71626 100644 --- a/packages/client-node/__tests__/integration/node_max_open_connections.test.ts +++ b/packages/client-node/__tests__/integration/node_max_open_connections.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { createSimpleTable } from "@test/fixtures/simple_table"; import { guid } from "@test/utils/guid"; import { sleep } from "@test/utils/sleep"; -import type { ClickHouseClient } from "../../src"; +import type { ClickHouseClient } from "@clickhouse/client"; import { createNodeTestClient } from "../utils/node_client"; describe("[Node.js] max_open_connections config", () => { diff --git a/packages/client-node/__tests__/integration/node_query_format_types.test.ts b/packages/client-node/__tests__/integration/node_query_format_types.test.ts index 8832b8007..a2f597a4f 100644 --- a/packages/client-node/__tests__/integration/node_query_format_types.test.ts +++ b/packages/client-node/__tests__/integration/node_query_format_types.test.ts @@ -5,7 +5,7 @@ import type { } from "@clickhouse/client-common"; import { createTableWithFields } from "@test/fixtures/table_with_fields"; import { guid } from "@test/utils/guid"; -import type { ClickHouseClient, ResultSet } from "../../src"; +import type { ClickHouseClient, ResultSet } from "@clickhouse/client"; import { createNodeTestClient } from "../utils/node_client"; /* eslint-disable @typescript-eslint/no-unused-expressions */ diff --git a/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts b/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts index 14c3996f4..da47af9dc 100644 --- a/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts +++ b/packages/client-node/__tests__/integration/node_response_headers_cap_client.test.ts @@ -1,6 +1,6 @@ import net, { type AddressInfo } from "net"; import { afterEach, describe, it } from "vitest"; -import { createClient } from "../../src"; +import { createClient } from "@clickhouse/client"; import type { ClickHouseClient } from "@clickhouse/client-common"; // Verifies that the Node.js client honors the `max_response_headers_size` diff --git a/packages/client-node/__tests__/integration/node_socket_handling.test.ts b/packages/client-node/__tests__/integration/node_socket_handling.test.ts index 6d8da0f1e..70bc88316 100644 --- a/packages/client-node/__tests__/integration/node_socket_handling.test.ts +++ b/packages/client-node/__tests__/integration/node_socket_handling.test.ts @@ -8,7 +8,7 @@ import { createTestClient } from "@test/utils/client"; import * as http from "http"; import net from "net"; import type Stream from "stream"; -import type { NodeClickHouseClientConfigOptions } from "../../src/config"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client"; import { AddressInfo } from "net"; const ClientTimeout = 10; // ms @@ -42,7 +42,7 @@ describe.concurrent("Slow server", () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); }); afterAll(async () => { await client.close(); @@ -195,7 +195,7 @@ describe("Resource is not available", () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); }); afterEach(async () => { // Free the fixed port between retries: otherwise a retry would find the @@ -247,7 +247,7 @@ describe.concurrent("Server that drops connections", () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); const result = await client.ping(); @@ -274,7 +274,7 @@ describe.concurrent("Server that drops connections", () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); const result = await client.ping(); @@ -303,7 +303,7 @@ describe.concurrent("Server that drops connections", () => { keep_alive: { enable: true, }, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); const result = await client.ping(); @@ -355,7 +355,7 @@ describe.concurrent("Server that drops connections", () => { level: 0, }, max_open_connections: 1, - } as NodeClickHouseClientConfigOptions); + } as ClickHouseClientConfigOptions); expect(await client.ping()).toMatchObject({ success: true }); diff --git a/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts b/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts index fa6ac381c..f6fe7777c 100644 --- a/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_error_handling.test.ts @@ -4,8 +4,8 @@ import { streamErrorQueryParams, } from "@test/fixtures/stream_errors"; import { isClickHouseVersionAtLeast } from "@test/utils/server_version"; -import type { ClickHouseClient } from "../../src"; -import type { ClickHouseError } from "../../src"; +import type { ClickHouseClient } from "@clickhouse/client"; +import type { ClickHouseError } from "@clickhouse/client"; import { createNodeTestClient } from "../utils/node_client"; // See https://github.com/ClickHouse/ClickHouse/pull/88818 diff --git a/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts b/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts index 7a971e1e0..0db2fa4a0 100644 --- a/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts +++ b/packages/client-node/__tests__/integration/node_stream_row_binary.test.ts @@ -1,28 +1,17 @@ -import { - ClickHouseLogLevel, - DefaultLogger, - LogWriter, - type ClickHouseClient, -} from "@clickhouse/client-common"; +import type { ClickHouseClient } from "@clickhouse/client-common"; import { describe, it, beforeEach, afterEach, expect } from "vitest"; import { createSimpleTable } from "@test/fixtures/simple_table"; import { createTestClient } from "@test/utils/client"; import { guid } from "@test/utils/guid"; import Stream from "stream"; -import { drainStreamInternal } from "../../src/connection/stream"; +import { drainStream } from "@clickhouse/client"; describe("[Node.js] stream RowBinary insert", () => { let client: ClickHouseClient; let tableName: string; - let log_writer: LogWriter; beforeEach(async () => { client = createTestClient(); - log_writer = new LogWriter( - new DefaultLogger(), - "Connection", - ClickHouseLogLevel.OFF, - ); tableName = `test_node_row_binary_stream_${guid()}`; await createSimpleTable(client, tableName); }); @@ -57,15 +46,7 @@ describe("[Node.js] stream RowBinary insert", () => { }); // The result stream contains nothing useful for an insert and should be // immediately drained to release the socket. - await drainStreamInternal( - { - op: "Insert", - query_id: execResult.query_id, - log_writer, - log_level: ClickHouseLogLevel.OFF, - }, - execResult.stream, - ); + await drainStream(execResult.stream); const rs = await client.query({ query: `SELECT * FROM ${tableName} ORDER BY id ASC`, diff --git a/packages/client-node/__tests__/integration/node_client.test.ts b/packages/client-node/__tests__/unit/node_client_headers.test.ts similarity index 98% rename from packages/client-node/__tests__/integration/node_client.test.ts rename to packages/client-node/__tests__/unit/node_client_headers.test.ts index e7f7e105c..c5ff39429 100644 --- a/packages/client-node/__tests__/integration/node_client.test.ts +++ b/packages/client-node/__tests__/unit/node_client_headers.test.ts @@ -1,8 +1,8 @@ import { vi, expect, it, describe, beforeEach, afterEach } from "vitest"; import { getHeadersTestParams } from "@test/utils/parametrized"; import Http from "http"; -import type { ClickHouseClient } from "../../src"; -import { createClient } from "../../src"; +import type { ClickHouseClient } from "@clickhouse/client"; +import { createClient } from "@clickhouse/client"; import { emitResponseBody, stubClientRequest } from "../utils/http_stubs"; describe("[Node.js] Client", () => { diff --git a/packages/client-node/__tests__/utils/assert.ts b/packages/client-node/__tests__/utils/assert.ts index 4b0994c4b..d266cb8fa 100644 --- a/packages/client-node/__tests__/utils/assert.ts +++ b/packages/client-node/__tests__/utils/assert.ts @@ -2,7 +2,7 @@ import { expect } from "vitest"; import type { ConnQueryResult } from "@clickhouse/client-common"; import { validateUUID } from "../../../client-common/__tests__/utils/guid"; import type Stream from "stream"; -import { getAsText } from "../../src/utils"; +import { getAsText } from "./stream"; export async function assertConnQueryResult( { stream, query_id }: ConnQueryResult, diff --git a/packages/client-node/__tests__/utils/node_client.ts b/packages/client-node/__tests__/utils/node_client.ts index 912913c8a..a73ed754a 100644 --- a/packages/client-node/__tests__/utils/node_client.ts +++ b/packages/client-node/__tests__/utils/node_client.ts @@ -3,7 +3,7 @@ import type Stream from "stream"; import type { ClickHouseClient, ClickHouseClientConfigOptions, -} from "../../src"; +} from "@clickhouse/client"; export function createNodeTestClient( config: ClickHouseClientConfigOptions = {}, diff --git a/packages/client-node/__tests__/utils/simple_node_client.ts b/packages/client-node/__tests__/utils/simple_node_client.ts index 2d515a80c..5ea4cc193 100644 --- a/packages/client-node/__tests__/utils/simple_node_client.ts +++ b/packages/client-node/__tests__/utils/simple_node_client.ts @@ -6,7 +6,7 @@ import type Stream from "stream"; import type { ClickHouseClient, ClickHouseClientConfigOptions, -} from "../../src"; +} from "@clickhouse/client"; export function createSimpleNodeTestClient( config: ClickHouseClientConfigOptions = {}, diff --git a/packages/client-node/__tests__/utils/stream.ts b/packages/client-node/__tests__/utils/stream.ts index 2e5f38460..3a0ddbcd9 100644 --- a/packages/client-node/__tests__/utils/stream.ts +++ b/packages/client-node/__tests__/utils/stream.ts @@ -17,3 +17,20 @@ export function makeObjectStream() { }, }); } + +// Test-local helper to fully read a stream into a string. This is a simplified +// stand-in for the client's internal `getAsText` — it deliberately omits the +// internal RangeError message normalization ("Invalid string length" / "Out of +// memory"), which these tests don't exercise. Kept here so the integration +// specs depend only on the published package surface (and stay runnable against +// the built `dist`). +export async function getAsText(stream: Stream.Readable): Promise { + let text = ""; + const textDecoder = new TextDecoder(); + for await (const chunk of stream) { + text += textDecoder.decode(chunk, { stream: true }); + } + // flush any unfinished multi-byte characters + text += textDecoder.decode(); + return text; +} diff --git a/packages/client-web/__tests__/integration/web_abort_request.test.ts b/packages/client-web/__tests__/integration/web_abort_request.test.ts index 7283808f5..3675110e0 100644 --- a/packages/client-web/__tests__/integration/web_abort_request.test.ts +++ b/packages/client-web/__tests__/integration/web_abort_request.test.ts @@ -1,13 +1,13 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import type { Row } from "@clickhouse/client-common"; import { createTestClient } from "@test/utils"; -import type { WebClickHouseClient } from "../../src/client"; +import type { ClickHouseClient } from "@clickhouse/client-web"; describe("[Web] abort request", () => { - let client: WebClickHouseClient; + let client: ClickHouseClient; beforeEach(() => { - client = createTestClient() as unknown as WebClickHouseClient; + client = createTestClient() as unknown as ClickHouseClient; }); afterEach(async () => { diff --git a/packages/client-web/__tests__/integration/web_client.test.ts b/packages/client-web/__tests__/integration/web_client.test.ts index 89924616b..d1b8de0c5 100644 --- a/packages/client-web/__tests__/integration/web_client.test.ts +++ b/packages/client-web/__tests__/integration/web_client.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { getHeadersTestParams } from "@test/utils/parametrized"; -import { createClient } from "../../src"; +import { createClient } from "@clickhouse/client-web"; describe("[Web] Client", () => { let fetchSpy: ReturnType; diff --git a/packages/client-web/__tests__/integration/web_error_parsing.test.ts b/packages/client-web/__tests__/integration/web_error_parsing.test.ts index e2d2c6582..da45f50c4 100644 --- a/packages/client-web/__tests__/integration/web_error_parsing.test.ts +++ b/packages/client-web/__tests__/integration/web_error_parsing.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { createClient } from "../../src"; +import { createClient } from "@clickhouse/client-web"; describe("[Web] errors parsing", () => { it("should return an error when URL is unreachable", async () => { diff --git a/packages/client-web/__tests__/integration/web_exec.test.ts b/packages/client-web/__tests__/integration/web_exec.test.ts index 7b72bd47f..9d2248410 100644 --- a/packages/client-web/__tests__/integration/web_exec.test.ts +++ b/packages/client-web/__tests__/integration/web_exec.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import type { ClickHouseClient } from "@clickhouse/client-common"; import { createTestClient } from "@test/utils"; -import { getAsText } from "../../src/utils"; -import { ResultSet } from "../../src"; +import { getAsText } from "../utils/stream"; +import { ResultSet } from "@clickhouse/client-web"; describe("[Web] exec result streaming", () => { let client: ClickHouseClient; diff --git a/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts b/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts index da648f220..d162c9972 100644 --- a/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts +++ b/packages/client-web/__tests__/integration/web_stream_error_handling.test.ts @@ -4,8 +4,8 @@ import { streamErrorQueryParams, } from "@test/fixtures/stream_errors"; import { isClickHouseVersionAtLeast } from "@test/utils/server_version"; -import type { ClickHouseClient } from "../../src"; -import type { ClickHouseError } from "../../src"; +import type { ClickHouseClient } from "@clickhouse/client-web"; +import type { ClickHouseError } from "@clickhouse/client-web"; import { createWebTestClient } from "../utils/web_client"; // See https://github.com/ClickHouse/ClickHouse/pull/88818 diff --git a/packages/client-web/__tests__/utils/simple_web_client.ts b/packages/client-web/__tests__/utils/simple_web_client.ts index 1daeaca7a..c1efc2664 100644 --- a/packages/client-web/__tests__/utils/simple_web_client.ts +++ b/packages/client-web/__tests__/utils/simple_web_client.ts @@ -2,11 +2,11 @@ // so that creating a simple client never registers the shared `beforeAll` // test-environment initializer and stays runnable without ClickHouse. import { createSimpleTestClient } from "@test/utils/simple_client"; -import type { ClickHouseClientConfigOptions } from "../../src"; -import type { WebClickHouseClient } from "../../src/client"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client-web"; +import type { ClickHouseClient } from "@clickhouse/client-web"; export function createSimpleWebTestClient( config: ClickHouseClientConfigOptions = {}, -): WebClickHouseClient { - return createSimpleTestClient(config) as unknown as WebClickHouseClient; +): ClickHouseClient { + return createSimpleTestClient(config) as unknown as ClickHouseClient; } diff --git a/packages/client-web/__tests__/utils/stream.ts b/packages/client-web/__tests__/utils/stream.ts new file mode 100644 index 000000000..06fb2ffdc --- /dev/null +++ b/packages/client-web/__tests__/utils/stream.ts @@ -0,0 +1,18 @@ +// Test-local helper to fully read a web ReadableStream into a string. This is a +// simplified stand-in for the client's internal `getAsText` — it deliberately +// omits the internal MaxStringLength guard and RangeError shaping, which these +// tests don't exercise. Kept here so the integration specs depend only on the +// published package surface (and stay runnable against the built `dist`). +export async function getAsText(stream: ReadableStream): Promise { + const textDecoder = new TextDecoder(); + const reader = stream.getReader(); + let text = ""; + let chunk = await reader.read(); + while (!chunk.done) { + text += textDecoder.decode(chunk.value, { stream: true }); + chunk = await reader.read(); + } + // flush any unfinished multi-byte characters + text += textDecoder.decode(); + return text; +} diff --git a/packages/client-web/__tests__/utils/web_client.ts b/packages/client-web/__tests__/utils/web_client.ts index 8fb77bdb5..b266e516c 100644 --- a/packages/client-web/__tests__/utils/web_client.ts +++ b/packages/client-web/__tests__/utils/web_client.ts @@ -1,9 +1,9 @@ import { createTestClient } from "@test/utils"; -import type { ClickHouseClientConfigOptions } from "../../src"; -import type { WebClickHouseClient } from "../../src/client"; +import type { ClickHouseClientConfigOptions } from "@clickhouse/client-web"; +import type { ClickHouseClient } from "@clickhouse/client-web"; export function createWebTestClient( config: ClickHouseClientConfigOptions = {}, -): WebClickHouseClient { - return createTestClient(config) as unknown as WebClickHouseClient; +): ClickHouseClient { + return createTestClient(config) as unknown as ClickHouseClient; } diff --git a/skills/clickhouse-js-node-rowbinary-parser/package-lock.json b/skills/clickhouse-js-node-rowbinary-parser/package-lock.json index 4a8e49dd2..ecaa39cde 100644 --- a/skills/clickhouse-js-node-rowbinary-parser/package-lock.json +++ b/skills/clickhouse-js-node-rowbinary-parser/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clickhouse/rowbinary", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@clickhouse/rowbinary", - "version": "0.1.1", + "version": "0.1.2", "license": "Apache-2.0", "dependencies": { "@clickhouse/datatype-parser": "^0.1.2" diff --git a/skills/clickhouse-js-node-rowbinary-parser/package.json b/skills/clickhouse-js-node-rowbinary-parser/package.json index ecc8c6476..fc10f2efe 100644 --- a/skills/clickhouse-js-node-rowbinary-parser/package.json +++ b/skills/clickhouse-js-node-rowbinary-parser/package.json @@ -1,6 +1,6 @@ { "name": "@clickhouse/rowbinary", - "version": "0.1.1", + "version": "0.1.2", "description": "RowBinary building blocks for Node.js — read/decode ClickHouse RowBinary / RowBinaryWithNames(AndTypes) streams (a matching writer is planned). Ships with the clickhouse-js-node-rowbinary-parser agent skill.", "homepage": "https://github.com/ClickHouse/clickhouse-js/tree/main/skills/clickhouse-js-node-rowbinary-parser", "license": "Apache-2.0", diff --git a/tests/e2e/install/src/integration.ts b/tests/e2e/install/src/integration.ts new file mode 100644 index 000000000..905011c8e --- /dev/null +++ b/tests/e2e/install/src/integration.ts @@ -0,0 +1,101 @@ +// Post-publish integration smoke against a real ClickHouse server. +// +// Where src/index.ts proves the freshly published @clickhouse/client installs +// and exposes the expected version + createClient, this exercises the installed +// artifact END-TO-END against a live server: connect, create/insert/select, +// stream, and confirm a bad query surfaces as a ClickHouseError. It runs in the +// publish workflow's e2e job (which installs the package by its published +// version and starts a single-node ClickHouse), so it validates the actual npm +// tarball a consumer would receive, not the local build. +const assert = require("assert"); +const { createClient, ClickHouseError } = require("@clickhouse/client"); + +// The e2e job starts ClickHouse via docker-compose just before this runs; poll +// ping briefly so we don't race the container coming up. +async function waitForClickHouse(client: any) { + const maxAttempts = 30; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const res = await client.ping(); + if (res.success) return; + } catch { + // not ready yet + } + if (attempt === maxAttempts) { + throw new Error("ClickHouse did not become available in time"); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +async function main() { + // Defaults target http://localhost:8123 with the default user, matching the + // single-node `clickhouse` service from docker-compose.yml. + const client = createClient(); + try { + await waitForClickHouse(client); + + const table = `e2e_install_${Date.now()}`; + await client.command({ + query: `CREATE TABLE ${table} (id UInt32, name String) ENGINE = MergeTree ORDER BY id`, + }); + + await client.insert({ + table, + values: [ + { id: 1, name: "foo" }, + { id: 2, name: "bar" }, + ], + format: "JSONEachRow", + }); + + const rs = await client.query({ + query: `SELECT id, name FROM ${table} ORDER BY id`, + format: "JSONEachRow", + }); + assert.deepStrictEqual(await rs.json(), [ + { id: 1, name: "foo" }, + { id: 2, name: "bar" }, + ]); + + // Streamed read. + const streamRs = await client.query({ + query: "SELECT number FROM system.numbers LIMIT 3", + format: "JSONEachRow", + }); + let streamed = 0; + for await (const rows of streamRs.stream()) { + streamed += rows.length; + } + assert.strictEqual(streamed, 3, "should stream 3 rows"); + + // A bad query must surface as a ClickHouseError instance from the SAME + // installed package (a single bundle => one class identity). + let caught: unknown; + try { + await client.query({ + query: "SELECT * FROM table_that_does_not_exist_e2e", + format: "JSONEachRow", + }); + } catch (err) { + caught = err; + } + assert.ok(caught, "expected an error for a bad query"); + assert.ok( + caught instanceof ClickHouseError, + "a server error should be a ClickHouseError instance", + ); + + await client.command({ query: `DROP TABLE ${table}` }); + console.log( + "OK: integration against the published @clickhouse/client passed", + ); + } finally { + await client.close(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/vitest.node.config.ts b/vitest.node.config.ts index 7171ab820..88268c89e 100644 --- a/vitest.node.config.ts +++ b/vitest.node.config.ts @@ -15,6 +15,26 @@ if ( ); } +// Which build of the client the `@clickhouse/*` specifiers resolve to: +// src (default) - the raw TypeScript sources, for a fast, build-free loop. +// dist - the compiled packages, exactly as a published consumer +// sees them (run `npm run build` first). An e2e-style guard +// against the built artifact / public surface. +// TEST_TARGET is orthogonal to TEST_MODE (which only selects the spec files), +// so e.g. `TEST_TARGET=dist TEST_MODE=integration` runs the integration specs +// against the built packages. Caveat: only specs that import EXCLUSIVELY via +// the `@clickhouse/*` names retarget cleanly; specs that also reach into +// `../../src` directly (most unit/integration specs do) keep importing source +// for those paths regardless. The `oss-dependents` collection imports only the +// published names, so it is a true built-surface guard and defaults to `dist`. +const testTarget = + process.env.TEST_TARGET ?? (testMode === "oss-dependents" ? "dist" : "src"); +if (testTarget !== "src" && testTarget !== "dist") { + throw new Error( + `Unsupported TEST_TARGET: [${testTarget}]. Supported targets are: src, dist.`, + ); +} + const collections = { unit: [ "packages/client-node/__tests__/unit/*.test.ts", @@ -39,11 +59,11 @@ const collections = { "packages/client-common/__tests__/integration/*.test.ts", ], // Runnable reproductions of how the top OSS dependents use the client. - // Unlike the other (fast, build-free) modes, these are e2e-style guards: they - // import the public package names and resolve to the BUILT workspace packages - // (see the resolve.alias note below), so a breaking change in the published - // surface fails the matching consumer's test. `npm run build` must run first. - // See packages/client-node/__tests__/oss-dependents. + // These specs import only the public package names, so they default to + // TEST_TARGET=dist (see above) and resolve to the BUILT workspace packages: + // a breaking change in the published surface fails the matching consumer's + // test. `npm run build` must run first. See + // packages/client-node/__tests__/oss-dependents. "oss-dependents": ["packages/client-node/__tests__/oss-dependents/*.test.ts"], all: [ "packages/client-common/__tests__/unit/*.test.ts", @@ -104,22 +124,39 @@ export default defineConfig({ retry: process.env.CI ? 2 : 0, }, resolve: { - // The oss-dependents specs import the published package names (`@clickhouse/ - // client`, `-web`, `-common`) exactly as the upstream dependents do; those - // resolve through the node_modules workspace symlinks to the BUILT packages - // (run `npm run build` first) — an e2e-style guard against the published - // surface rather than `src`. `@clickhouse/client-node` is not a real package - // name (the node client publishes as `@clickhouse/client`); it is an - // internal alias the shared node setup/util files import, so we repoint it - // at the built node `dist` for this mode. Every other mode aliases the - // workspace `src` instead for a fast, build-free unit/integration loop. + // Driven by TEST_TARGET (see above). With `dist`, the published package + // names (`@clickhouse/client`, `-web`, `-common`) resolve through the + // node_modules workspace symlinks to the BUILT packages (run `npm run build` + // first) — an e2e-style guard against the published surface. With `src`, + // they alias the workspace sources for a fast, build-free loop. + // `@clickhouse/client-node` is not a real package name (the node client + // publishes as `@clickhouse/client`); it is an internal alias the shared + // node setup/util files import, so under `dist` we repoint it at the built + // node `dist`. + // + // Under `dist`, the node and common specifiers aliased below + // (`@clickhouse/client`, `@clickhouse/client-common`, and the internal + // `@clickhouse/client-node`) all resolve to the node client's own bundle. + // The node client bundles the common sources (client-common is deprecated + // and not a runtime dep), so a real consumer gets common-origin symbols — + // `ClickHouseError`, value classes like `SettingsMap`/`TupleParam` — from + // `@clickhouse/client`, not from a separate `client-common`. Pointing them + // at one bundle keeps a single class identity, so the client's internal + // `instanceof` checks on test-provided values (and the tests' own + // `instanceof` assertions) hold. (`@clickhouse/client-web`, imported by the + // oss-dependents suite, is not aliased here — it resolves through + // node_modules to the web client's own dist.) alias: - testMode === "oss-dependents" + testTarget === "dist" ? { + "@clickhouse/client": "packages/client-node/dist", + "@clickhouse/client-common": "packages/client-node/dist", "@clickhouse/client-node": "packages/client-node/dist", "@test": "packages/client-common/__tests__", } : { + // The published node name, imported by the integration specs. + "@clickhouse/client": "packages/client-node/src", "@clickhouse/client-common": "packages/client-common/src", "@clickhouse/client-node": "packages/client-node/src", "@test": "packages/client-common/__tests__", diff --git a/vitest.web.config.ts b/vitest.web.config.ts index 1c590f3e9..a4080bf67 100644 --- a/vitest.web.config.ts +++ b/vitest.web.config.ts @@ -23,6 +23,23 @@ if ( ); } +// Which build of the client the `@clickhouse/*` specifiers resolve to: +// src (default) - the raw TypeScript sources (via the `unittest` export +// condition), for a fast, build-free loop. +// dist - the compiled packages, exactly as a published consumer +// sees them (run `npm run build` first). An e2e-style guard +// against the built artifact / public surface. +// TEST_TARGET is orthogonal to TEST_MODE (which only selects the spec files). +// Caveat: only specs that import EXCLUSIVELY via the `@clickhouse/*` names +// retarget cleanly; specs that also reach into `../../src` directly keep +// importing source for those paths regardless. +const testTarget = process.env.TEST_TARGET ?? "src"; +if (testTarget !== "src" && testTarget !== "dist") { + throw new Error( + `Unsupported TEST_TARGET: [${testTarget}]. Supported targets are: src, dist.`, + ); +} + const collections = { unit: [ "packages/client-common/__tests__/unit/*.test.ts", @@ -120,19 +137,47 @@ export default defineConfig({ instances: [{ browser }], }, }, + // In `dist` mode the web client resolves to its published CJS bundle; force + // Vite to pre-bundle it (as a real bundler-based consumer would) so its named + // exports are exposed to the browser ESM imports. + optimizeDeps: + testTarget === "dist" ? { include: ["@clickhouse/client-web"] } : undefined, resolve: { - // Use the unittest entry point to get the source files instead of built files - conditions: ["unittest"], - alias: { - "@clickhouse/client-common": fileURLToPath( - new URL("./packages/client-common/src", import.meta.url), - ), - "@clickhouse/client-web": fileURLToPath( - new URL("./packages/client-web", import.meta.url), - ), - "@test": fileURLToPath( - new URL("./packages/client-common/__tests__", import.meta.url), - ), - }, + // Driven by TEST_TARGET (see above). With `src` (default), the `unittest` + // export condition + aliases resolve the raw sources. With `dist`, we drop + // them so the published `default` export (dist/index.js) and the + // node_modules workspace symlinks resolve to the BUILT packages (run + // `npm run build` first). + conditions: testTarget === "dist" ? [] : ["unittest"], + // Under `dist`, both client specifiers resolve to the web client's own + // bundle. The web client bundles the common sources (client-common is + // deprecated and not a runtime dep), so a real consumer gets common-origin + // symbols — value classes like `SettingsMap`/`TupleParam`, `ClickHouseError` + // — from `@clickhouse/client-web`. Pointing both at one bundle keeps a + // single class identity, so `instanceof` checks (the client's internal ones + // on test-provided values, and the tests' own) hold. + alias: + testTarget === "dist" + ? { + // Redirect the deprecated common package NAME to the web client + // package NAME (not a path) so it resolves through the published + // entry — Vite pre-bundles the CJS dist and its named exports stay + // intact, and common-origin symbols share the client's one bundle. + "@clickhouse/client-common": "@clickhouse/client-web", + "@test": fileURLToPath( + new URL("./packages/client-common/__tests__", import.meta.url), + ), + } + : { + "@clickhouse/client-common": fileURLToPath( + new URL("./packages/client-common/src", import.meta.url), + ), + "@clickhouse/client-web": fileURLToPath( + new URL("./packages/client-web", import.meta.url), + ), + "@test": fileURLToPath( + new URL("./packages/client-common/__tests__", import.meta.url), + ), + }, }, });