diff --git a/AGENTS.md b/AGENTS.md index 10d1ec63a..8c6359180 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,9 @@ It captures practical rules that prevent avoidable CI and PR churn. - Run the focused test against the pre-fix implementation and confirm it fails for the expected reason. - Apply the implementation change, rerun the same test, and confirm it passes. - Report the red-green evidence in the PR verification summary. +- Import boundaries are enforced by Biome (`biome.json`): + - Modules import core only from the `testcontainers` entry point. The one allowed exception is the core test helper (`../../../testcontainers/src/utils/test-helper`). + - `noImportCycles` is an error. Prefer breaking a new cycle (for example with `import type` or a leaf module) over adding a `biome-ignore` suppression. - Test-only helper files under `src` (for example `*-test-utils.ts`) must be explicitly excluded from package `tsconfig.build.json` so they are not emitted into `build` and accidentally published. - For substantial changes to GitHub Actions, runner images, Node/npm versions, or release/publish automation, consider running the manual `Node.js Package` workflow as a dry-run publish sanity check. - Select the PR branch as the workflow ref to test publish workflow changes before merging. diff --git a/biome.json b/biome.json index e1744c9a7..5cd43e809 100644 --- a/biome.json +++ b/biome.json @@ -28,6 +28,9 @@ }, "nursery": { "noFloatingPromises": "error" + }, + "suspicious": { + "noImportCycles": "error" } } }, @@ -41,6 +44,30 @@ } } } + }, + { + "includes": ["packages/modules/**"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "testcontainers/**", + "**/testcontainers/src/**", + "!**/testcontainers/src/utils/test-helper" + ], + "message": "Modules must import from the public \"testcontainers\" entry point, not its internals." + } + ] + } + } + } + } + } } ], "assist": { diff --git a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts index fa6dd7a3e..4dfb1a228 100644 --- a/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts +++ b/packages/modules/mongodb/src/mongodb-atlas-local-container.test.ts @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import { IntervalRetry } from "../../../testcontainers/src/common"; +import { IntervalRetry } from "testcontainers"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { MongoDBAtlasLocalContainer } from "./mongodb-atlas-local-container"; diff --git a/packages/modules/weaviate/src/weaviate-container.test.ts b/packages/modules/weaviate/src/weaviate-container.test.ts index 5db9d65ea..dee64e9e7 100644 --- a/packages/modules/weaviate/src/weaviate-container.test.ts +++ b/packages/modules/weaviate/src/weaviate-container.test.ts @@ -1,4 +1,3 @@ -import type { Environment } from "testcontainers/src/types"; import weaviate from "weaviate-ts-client"; import { getImage } from "../../../testcontainers/src/utils/test-helper"; import { WeaviateContainer } from "./weaviate-container"; @@ -38,7 +37,7 @@ describe("WeaviateContainer", { timeout: 100_000 }, () => { "text2vec-huggingface", "generative-openai", ]; - const environment: Environment = { + const environment = { ENABLE_MODULES: enableModules.join(","), BACKUP_FILESYSTEM_PATH: "/tmp/backups", }; diff --git a/packages/testcontainers/src/generic-container/generic-container-builder.ts b/packages/testcontainers/src/generic-container/generic-container-builder.ts index a2778e27a..d06a7867e 100644 --- a/packages/testcontainers/src/generic-container/generic-container-builder.ts +++ b/packages/testcontainers/src/generic-container/generic-container-builder.ts @@ -3,11 +3,13 @@ import type { ImageBuildOptions } from "dockerode"; import { log, RandomUuid, type Uuid } from "../common"; import { getAuthConfig, getContainerRuntimeClient, ImageName } from "../container-runtime"; import type { AuthConfig } from "../container-runtime/auth/types"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import type { BuildArgs, RegistryConfig } from "../types"; import { getDockerfileImages } from "../utils/dockerfile-parser"; import { createLabels, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { type ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; +// biome-ignore lint/suspicious/noImportCycles: fromDockerfile() and build() construct each other; only dereferenced at runtime import { GenericContainer } from "./generic-container"; export type BuildOptions = { diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 7d74006c1..d9b2f8ffd 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -7,7 +7,9 @@ import { containerLog, hash, log, toNanos } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types"; import type { StartedNetwork } from "../network/network"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { PortForwarderInstance, SSHD_IMAGE } from "../port-forwarder/port-forwarder"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper, getReaperImage } from "../reaper/reaper"; import type { StartedTestContainer, TestContainer } from "../test-container"; import type { @@ -34,8 +36,10 @@ import { type ImagePullPolicy, PullPolicy } from "../utils/pull-policy"; import { selectWaitStrategy } from "../wait-strategies/utils/wait-strategy-selector"; import { waitForContainer } from "../wait-strategies/wait-for-container"; import type { WaitStrategy } from "../wait-strategies/wait-strategy"; +// biome-ignore lint/suspicious/noImportCycles: fromDockerfile() and build() construct each other; only dereferenced at runtime import { GenericContainerBuilder } from "./generic-container-builder"; import { inspectContainerUntilPortsExposed } from "./inspect-container-util-ports-exposed"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { StartedGenericContainer } from "./started-generic-container"; const reusableContainerCreationLock = new AsyncLock(); diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index 3abceb934..fa23559ad 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -6,6 +6,7 @@ import type Dockerode from "dockerode"; import type { ContainerInspectInfo } from "dockerode"; import { containerLog, log } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import type { RestartOptions, StartedTestContainer, StopOptions, StoppedTestContainer } from "../test-container"; import type { diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.ts index 4eb7bffd7..ba64dc8a8 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.ts @@ -3,7 +3,9 @@ import type { ContainerInfo } from "dockerode"; import { createSshConnection, type SshConnection } from "ssh-remote-port-forward"; import { log, withFileLock } from "../common"; import { type ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { GenericContainer } from "../generic-container/generic-container"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { getReaper } from "../reaper/reaper"; import { LABEL_TESTCONTAINERS_SESSION_ID, LABEL_TESTCONTAINERS_SSHD } from "../utils/labels"; import type { PortWithOptionalBinding } from "../utils/port"; diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index 6d4d69c0c..99f3ad395 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -3,6 +3,7 @@ import { userInfo } from "node:os"; import type { ContainerInfo } from "dockerode"; import { IntervalRetry, log, RandomUuid, withFileLock } from "../common"; import { type ContainerRuntimeClient, ImageName } from "../container-runtime"; +// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime import { GenericContainer } from "../generic-container/generic-container"; import { LABEL_TESTCONTAINERS_LANG, LABEL_TESTCONTAINERS_RYUK, LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; import { Wait } from "../wait-strategies/wait";