diff --git a/.github/workflows/github-scripts-ci.yml b/.github/workflows/github-scripts-ci.yml index ada4560ee1..b7ce2a3df5 100644 --- a/.github/workflows/github-scripts-ci.yml +++ b/.github/workflows/github-scripts-ci.yml @@ -46,11 +46,12 @@ jobs: - name: Run tests run: | set -uo pipefail - # The leading "./" is load-bearing: `bun test .github/scripts` - # (without it) silently discovers ZERO tests and still exits 0. - # Capture output to a file instead of piping it, so `test_exit` - # below is `bun test`'s own exit code, not `tee`/`grep`'s. - bun test ./.github/scripts > /tmp/github-scripts-test-output.txt 2>&1 + # The root `test:github-scripts` script runs `bun test ./.github/scripts`; + # its leading "./" is load-bearing, since `bun test .github/scripts` + # silently discovers ZERO tests and still exits 0. Capture output to a + # file instead of piping it, so `test_exit` below is the script's own + # exit code, not `tee`/`grep`'s. + pnpm run test:github-scripts > /tmp/github-scripts-test-output.txt 2>&1 test_exit=$? cat /tmp/github-scripts-test-output.txt if [ "$test_exit" -ne 0 ]; then diff --git a/apps/cli/src/shared/auth/api.service.ts b/apps/cli/src/shared/auth/api.service.ts index 5ecb0466fc..743ce06f23 100644 --- a/apps/cli/src/shared/auth/api.service.ts +++ b/apps/cli/src/shared/auth/api.service.ts @@ -9,7 +9,7 @@ export type LoginSessionResponse = { nonce: string; }; -export type ProfileResponse = { +type ProfileResponse = { gotrue_id: string; primary_email: string; username: string; diff --git a/apps/cli/src/shared/config/project-link-remote.service.ts b/apps/cli/src/shared/config/project-link-remote.service.ts deleted file mode 100644 index bc65400208..0000000000 --- a/apps/cli/src/shared/config/project-link-remote.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Effect } from "effect"; -import { Context } from "effect"; -import type { LinkedServiceVersions } from "./project-link-state.service.ts"; - -const linkedProjectVersionServices = ["postgres", "postgrest", "auth", "storage"] as const; - -type LinkedProjectVersionService = (typeof linkedProjectVersionServices)[number]; - -interface AccessibleProject { - readonly ref: string; - readonly name: string; - readonly region: string; - readonly status: string; - readonly organizationId: string; - readonly organizationSlug: string; -} - -interface LinkedProjectSnapshot extends AccessibleProject { - readonly versions: LinkedServiceVersions; - readonly unavailableServices: ReadonlyArray; -} - -interface ProjectLinkRemoteShape { - readonly listAccessibleProjects: Effect.Effect, unknown>; - readonly fetchLinkedProject: ( - projectRef: string, - ) => Effect.Effect; -} - -export class ProjectLinkRemote extends Context.Service()( - "supabase/cli/ProjectLinkRemote", -) {} diff --git a/apps/cli/src/shared/config/project-link-state.service.ts b/apps/cli/src/shared/config/project-link-state.service.ts index b78b834fb6..363de89e71 100644 --- a/apps/cli/src/shared/config/project-link-state.service.ts +++ b/apps/cli/src/shared/config/project-link-state.service.ts @@ -13,8 +13,6 @@ const LinkedServiceVersionsSchema = Schema.Struct({ storage: Schema.optionalKey(Schema.String), }); -export type LinkedServiceVersions = Schema.Schema.Type; - const ActiveBranchSchema = Schema.Struct({ ref: Schema.String, name: Schema.String, diff --git a/apps/cli/src/shared/telemetry/event-catalog.ts b/apps/cli/src/shared/telemetry/event-catalog.ts index 513828a59c..5720929844 100644 --- a/apps/cli/src/shared/telemetry/event-catalog.ts +++ b/apps/cli/src/shared/telemetry/event-catalog.ts @@ -28,7 +28,6 @@ export const PropCliVersion = "cli_version"; export const PropEnvSignals = "env_signals"; export const PropCommandRunId = "command_run_id"; export const PropCommand = "command"; -export const PropFlags = "flags"; export const PropExitCode = "exit_code"; export const PropDurationMs = "duration_ms"; export const PropOutputFormat = "output_format"; diff --git a/apps/cli/tests/helpers/cli.ts b/apps/cli/tests/helpers/cli.ts index 56b7d0a229..066ce33e4a 100644 --- a/apps/cli/tests/helpers/cli.ts +++ b/apps/cli/tests/helpers/cli.ts @@ -291,7 +291,7 @@ function outputTail(label: string, output: string): string { return `${label}:\n${tail}`; } -export function spawnSupabase( +function spawnSupabase( args: string[], options?: { cwd?: string; diff --git a/apps/cli/tests/helpers/command-mocks.ts b/apps/cli/tests/helpers/command-mocks.ts index 5b2bfe35fd..f2c2a86ea8 100644 --- a/apps/cli/tests/helpers/command-mocks.ts +++ b/apps/cli/tests/helpers/command-mocks.ts @@ -71,7 +71,7 @@ import { export const VALID_REF = "abcdefghijklmnopqrst"; export const VALID_TOKEN = "sbp_" + "a".repeat(40); export const DEFAULT_API_URL = "https://api.supabase.com"; -export const DEFAULT_USER_AGENT = "SupabaseCLI/0.0.0-dev"; +const DEFAULT_USER_AGENT = "SupabaseCLI/0.0.0-dev"; // No-op layers — drop-in for tests that don't assert on telemetry / cache state. export const mockLinkedProjectCacheLayer = Layer.succeed(LinkedProjectCache, { @@ -468,7 +468,7 @@ function makeHttpClientLayer( // otherwise the raw decoded string is stored. Falsy bodies (no request body) record `undefined`. export type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; -export interface RecordedRequest { +interface RecordedRequest { readonly url: string; readonly method: string; readonly headers: Readonly>; diff --git a/apps/cli/tests/helpers/compute.ts b/apps/cli/tests/helpers/compute.ts index 9605fdd9fc..8d9f2628be 100644 --- a/apps/cli/tests/helpers/compute.ts +++ b/apps/cli/tests/helpers/compute.ts @@ -26,7 +26,7 @@ import { mockOutput, mockProcessControl, mockRuntimeInfo, mockTty } from "./mock export const COMPUTE_PROJECT_REF = "abcdefghijklmnopqrst"; -export interface RecordedRequest { +interface RecordedRequest { readonly method: string; readonly url: string; /** @@ -41,7 +41,7 @@ export interface RecordedRequest { readonly byteLength: number; } -export interface StubResponse { +interface StubResponse { readonly status: number; readonly body?: unknown; } @@ -51,7 +51,7 @@ export interface StubResponse { * Distinct from a `StubResponse` with an error status, which is a server that * answered. */ -export interface StubTransportFailure { +interface StubTransportFailure { readonly transportError: string; } @@ -62,7 +62,7 @@ function isTransportFailure( } /** How a test answers one request; sequential entries reply to repeated calls. */ -export type RouteHandler = +type RouteHandler = | StubResponse | StubTransportFailure | ReadonlyArray; @@ -100,7 +100,7 @@ function isRouteSequence( * build-context upload, so a test can assert the whole request sequence — mint * the slot, PUT the bytes, deploy, poll — in the order it happened. */ -export function mockComputeHttp(routes: ComputeHttpRoutes) { +function mockComputeHttp(routes: ComputeHttpRoutes) { const requests: Array = []; const remaining = new Map>( Object.entries(routes).map(([route, handler]) => [ diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 828321e128..6d13ba887b 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -14,7 +14,7 @@ export type LiveProject = LiveCliProjectEnvironment["project"]; type RunOptions = NonNullable[1]>; type RunResult = Awaited>; -export interface LiveWorkspace { +interface LiveWorkspace { readonly path: string; } diff --git a/apps/cli/tests/helpers/mocks.ts b/apps/cli/tests/helpers/mocks.ts index d1176dde2a..09451ce931 100644 --- a/apps/cli/tests/helpers/mocks.ts +++ b/apps/cli/tests/helpers/mocks.ts @@ -2,20 +2,14 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import process from "node:process"; import { BunServices } from "@effect/platform-bun"; -import { Deferred, Effect, Layer, Option, Redacted, Stream } from "effect"; +import { Deferred, Effect, Layer, Option, Stream } from "effect"; import type { CliProjectEnvironment, CliProjectPaths } from "@supabase/config"; -import { Api } from "../../src/shared/auth/api.service.ts"; -import type { LoginSessionResponse, ProfileResponse } from "../../src/shared/auth/api.service.ts"; -import { Credentials } from "../../src/shared/auth/credentials.service.ts"; -import { Crypto } from "../../src/shared/auth/crypto.service.ts"; -import { ApiError } from "../../src/shared/auth/errors.ts"; import { cliSettingsLayer } from "../../src/shared/config/cli-settings.layer.ts"; import { CliProjectHome } from "../../src/shared/config/cli-project-home.service.ts"; import { CliProjectLocalServiceVersions, type LocalServiceVersionsState, } from "../../src/shared/config/cli-project-local-service-versions.service.ts"; -import { ProjectLinkRemote } from "../../src/shared/config/project-link-remote.service.ts"; import { ProjectLinkState, type ProjectLinkStateValue, @@ -70,18 +64,6 @@ export function mockBrowser(): Layer.Layer { }); } -export function mockCrypto(token = "sbp_" + "a".repeat(40)): Layer.Layer { - return Layer.succeed(Crypto, { - generateKeyPair: Effect.sync(() => ({ - ecdh: {} as import("node:crypto").ECDH, - publicKeyHex: "04abcd", - })), - generateSessionId: Effect.sync(() => "test-session-id"), - defaultTokenName: Effect.sync(() => "cli_test@host_123"), - decryptToken: () => Effect.succeed(token), - }); -} - export function mockStdin(isTTY: boolean, pipedInput?: string | Uint8Array): Layer.Layer { const pipedBytes = pipedInput === undefined @@ -200,33 +182,6 @@ export function mockProcessControl( }; } -export function mockCredentials(opts: { existingToken?: string } = {}) { - let savedToken: string | undefined; - let deleteWasCalled = false; - return { - layer: Layer.succeed(Credentials, { - getAccessToken: Effect.sync(() => { - const token = opts.existingToken ?? savedToken; - return token ? Option.some(Redacted.make(token)) : Option.none(); - }), - saveAccessToken: (token: string | Redacted.Redacted) => - Effect.sync(() => { - savedToken = typeof token === "string" ? token : Redacted.value(token); - }), - deleteAccessToken: Effect.sync(() => { - deleteWasCalled = true; - return !!(opts.existingToken ?? savedToken); - }), - }), - get savedToken() { - return savedToken; - }, - get deleteWasCalled() { - return deleteWasCalled; - }, - }; -} - export function mockOutput( opts: { format?: OutputFormat; @@ -470,56 +425,6 @@ export function mockOutput( }; } -export function mockApi( - opts: { - failTimes?: number; - response?: Partial; - profileResponse?: Partial; - profileError?: ApiError; - } = {}, -) { - let callCount = 0; - let profileCallCount = 0; - const failTimes = opts.failTimes ?? 0; - const response: LoginSessionResponse = { - access_token: "encrypted", - public_key: "abcd", - nonce: "1234", - ...opts.response, - }; - const profileResponse: ProfileResponse = { - gotrue_id: "user-123", - primary_email: "test@example.com", - username: "tester", - ...opts.profileResponse, - }; - - return { - layer: Layer.succeed(Api, { - fetchLoginSession: () => { - callCount++; - if (callCount <= failTimes) { - return Effect.fail(new ApiError({ detail: "network error" })); - } - return Effect.succeed(response); - }, - fetchProfile: () => { - profileCallCount++; - if (opts.profileError !== undefined) { - return Effect.fail(opts.profileError); - } - return Effect.succeed(profileResponse); - }, - }), - get callCount() { - return callCount; - }, - get profileCallCount() { - return profileCallCount; - }, - }; -} - /** * Like `mockAnalytics()`, but merges `CurrentAnalyticsContext` into captured event * properties. Use it when asserting on context-carried fields (`flags`, `groups`). @@ -720,7 +625,7 @@ function mockCliProjectHome( ); } -export function mockProjectLinkState( +function mockProjectLinkState( initialState?: ProjectLinkStateValue, ): Layer.Layer { let state = initialState; @@ -751,62 +656,7 @@ export function mockProjectLinkState( ); } -export function mockProjectLinkRemote( - opts: { - projects?: ReadonlyArray<{ - ref: string; - name: string; - region: string; - status: string; - organizationId?: string; - organizationSlug?: string; - }>; - linkedProject?: { - ref: string; - name: string; - region: string; - status: string; - organizationId?: string; - organizationSlug?: string; - versions: { - postgres?: string; - postgrest?: string; - auth?: string; - storage?: string; - }; - unavailableServices?: ReadonlyArray<"postgres" | "postgrest" | "auth" | "storage">; - }; - } = {}, -): Layer.Layer { - const projects = opts.projects ?? []; - const linkedProject = opts.linkedProject; - return Layer.succeed( - ProjectLinkRemote, - ProjectLinkRemote.of({ - listAccessibleProjects: Effect.succeed( - projects.map((project) => ({ - ...project, - organizationId: project.organizationId ?? "org_123", - organizationSlug: project.organizationSlug ?? "supabase", - })), - ), - fetchLinkedProject: (projectRef: string) => - Effect.gen(function* () { - if (linkedProject === undefined) { - return yield* Effect.fail(new Error(`No linked project mock for ${projectRef}`)); - } - return { - ...linkedProject, - organizationId: linkedProject.organizationId ?? "org_123", - organizationSlug: linkedProject.organizationSlug ?? "supabase", - unavailableServices: linkedProject.unavailableServices ?? [], - }; - }), - }), - ); -} - -export function mockCliProjectLocalServiceVersions( +function mockCliProjectLocalServiceVersions( initialState?: LocalServiceVersionsState, ): Layer.Layer { let state = initialState; @@ -843,23 +693,3 @@ export function emptyEnv() { cliSettingsLayer.pipe(Layer.provide(runtimeInfoLayer), Layer.provide(cliProjectContextLayer)), ); } - -export function withEnv(env: Record) { - const runtimeInfoLayer = mockRuntimeInfo(); - const cliProjectContextLayer = mockCliProjectContext(); - const envLayer = processEnvLayer(env); - const cliProjectHomeLayer = mockCliProjectHome(); - const analytics = mockAnalytics(); - return Layer.mergeAll( - BunServices.layer, - runtimeInfoLayer, - cliProjectContextLayer, - cliProjectHomeLayer, - analytics.layer, - mockTelemetryRuntime(), - envLayer, - mockTty(), - mockProcessControl().layer, - cliSettingsLayer.pipe(Layer.provide(runtimeInfoLayer), Layer.provide(cliProjectContextLayer)), - ); -} diff --git a/apps/cli/tests/helpers/npm-registry.ts b/apps/cli/tests/helpers/npm-registry.ts index 0d2f30a4af..bc9f36015f 100644 --- a/apps/cli/tests/helpers/npm-registry.ts +++ b/apps/cli/tests/helpers/npm-registry.ts @@ -266,7 +266,7 @@ listen: 0.0.0.0:${PORT} // Sync versions across all packages console.log(`Syncing versions to ${version}...`); - await $`pnpm exec bun apps/cli/scripts/sync-versions.ts --version ${version}`.cwd(root).quiet(); + await $`bun ../../scripts/sync-versions.ts --version ${version}`.cwd(import.meta.dir).quiet(); console.log("Starting local npm registry..."); await using registry = await startVerdaccio(configPath, PORT); diff --git a/apps/cli/tests/helpers/storage.ts b/apps/cli/tests/helpers/storage.ts index 30d3bd0885..bc391afba0 100644 --- a/apps/cli/tests/helpers/storage.ts +++ b/apps/cli/tests/helpers/storage.ts @@ -31,7 +31,7 @@ import { * or recursive flows need one route per expected call unless `persist` is set. * `when` narrows a match by the parsed request body. */ -export interface StorageRoute { +interface StorageRoute { readonly method: string; /** Substring matched against the request URL. */ readonly match: string; @@ -49,7 +49,7 @@ export interface StorageRoute { readonly persist?: boolean; } -export interface RecordedStorageRequest { +interface RecordedStorageRequest { readonly method: string; readonly url: string; readonly headers: Record; diff --git a/knip.json b/knip.json deleted file mode 100644 index 72207b2ce5..0000000000 --- a/knip.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "./node_modules/knip/schema.json", - "exclude": ["catalogReferences"], - "workspaces": { - ".": { - "entry": [".github/scripts/**/*.ts", "tools/release/*.ts"], - "ignore": [".repos/**", "apps/cli-go/**"], - "ignoreBinaries": ["go"], - "ignoreDependencies": ["verdaccio"] - }, - "apps/cli": { - "entry": [ - "src/shared/cli/bin.ts", - "src/**/*.test.ts", - "src/**/*.e2e.test.ts", - "src/**/*.live.test.ts", - "scripts/analyze-commits-title.js" - ], - "ignore": [ - "scripts/*.ts", - "tests/**/*.ts", - "src/shared/feedback/database.types.ts", - "src/shared/telemetry/event-catalog.ts", - "src/shared/compute/stacks/**" - ], - "ignoreBinaries": ["mkfifo"], - "ignoreDependencies": ["prettier"] - }, - "apps/cli-e2e": { - "entry": ["src/**/*.e2e.test.ts", "tests/**/*.ts"] - }, - "apps/docs": { - "entry": ["mdx-components.tsx"] - }, - "packages/api": { - "entry": ["src/**/*.test.ts"], - "ignoreDependencies": ["undici"] - }, - "packages/config": { - "entry": ["src/**/*.test.ts"] - }, - "packages/stack": { - "ignoreDependencies": ["@types/ws", "ws"] - }, - "packages/cli-test-helpers": { - "entry": ["src/**/*.test.ts"] - } - } -} diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 0000000000..2d52beafc1 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,66 @@ +{ + "$schema": "./node_modules/knip/schema.json", + // Required in the event that git submodules are present. Do not remove this, despite knip warnings. + "ignore": [".repos/**"], + // `go` comes from mise. + "ignoreBinaries": ["go"], + // Spawned through a node_modules/.bin path in tools/release/local-registry.ts and + // bunx in apps/cli/tests/helpers/npm-registry.ts, which knip cannot trace. + "ignoreDependencies": ["verdaccio"], + "ignoreWorkspaces": ["apps/cli-go"], + "workspaces": { + "apps/cli": { + "entry": [ + // Bundled by `bun build` in the build:shim script; knip's bun resolver derives no + // entry from `bun build`. + "src/shared/cli/bin.ts", + // Run from workflow steps. Declared because publish-docs-spec.ts is pipe-invoked and + // because a root plugin with a `**/` pattern makes knip drop cross-workspace workflow + // entries. + "scripts/*.ts", + // Referenced from package.json#release.plugins; knip's Semantic Release plugin only + // reads the root manifest. + "scripts/analyze-commits-title.js", + // tests/smoke-test.ts picks the platform file from a string map and spawns it. + "tests/smoke-test-*.ts", + ], + "ignore": [ + // Generated by the gen:feedback-types script. + "src/shared/feedback/database.types.ts", + // Starter templates read as text by compute-stacks.macro.ts, never imported. + "src/shared/compute/stacks/**", + ], + // Binaries the machine provides rather than npm. + "ignoreBinaries": [ + // apps/cli/tests/smoke-test-macos.ts + "brew", + // apps/cli/tests/smoke-test-windows.ts + "cygpath", + // apps/cli/src/commands/seed/buckets/buckets.integration.test.ts + "mkfifo", + // apps/cli/scripts/build.ts + "nfpm", + // apps/cli/scripts/build.ts + "rcodesign", + // apps/cli/tests/smoke-test-windows.ts + "scoop", + // The CLI under test in apps/cli/tests/smoke-test-{macos,windows}.ts. + "supabase", + ], + // TODO(CLI-2424): spawned through a node_modules/.bin path in scripts/publish-docs-spec.ts. + "ignoreDependencies": ["prettier"], + }, + "apps/docs": { + // Next.js convention file that knip's Next.js plugin does not list. + "entry": ["mdx-components.tsx"], + }, + "packages/config": { + // Run from workflow steps; declared for the same cross-workspace reason as apps/cli. + "entry": ["scripts/*.ts"], + }, + "packages/stack": { + // TODO(CLI-2423): knip does not credit the ws import in src/public/whole-stack.e2e.test.ts. + "ignoreDependencies": ["@types/ws", "ws"], + }, + }, +} diff --git a/package.json b/package.json index 4335c748ff..748c796942 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test:live": "pnpm exec turbo run supabase#test:live --concurrency=1 --", "record": "pnpm exec turbo run @supabase/cli-e2e#record --", "test:smoke": "pnpm exec turbo run supabase#test:smoke --", + "test:github-scripts": "bun test ./.github/scripts", "dev:docs": "pnpm exec turbo run @supabase/docs#dev", "test:unit": "pnpm exec turbo run test:unit:run --filter=!@supabase/cli-go --", "test:integration": "pnpm exec turbo run test:integration:run --", diff --git a/packages/api/package.json b/packages/api/package.json index 13bdbaa8cf..538628d160 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,8 +23,7 @@ "dependencies": { "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", - "effect": "catalog:", - "undici": "^8.10.0" + "effect": "catalog:" }, "devDependencies": { "@tsconfig/bun": "catalog:", diff --git a/packages/config/scripts/release-plan.ts b/packages/config/scripts/release-plan.ts index 4bf3d2b049..5327a5612c 100644 --- a/packages/config/scripts/release-plan.ts +++ b/packages/config/scripts/release-plan.ts @@ -30,7 +30,7 @@ interface ConfigPackageJson { readonly private?: boolean; } -export interface ReleaseDuePlan { +interface ReleaseDuePlan { readonly due: true; readonly version: string; readonly bumpType: string; @@ -38,7 +38,7 @@ export interface ReleaseDuePlan { readonly isPrivate: boolean; } -export interface NoReleasePlan { +interface NoReleasePlan { readonly due: false; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b61093fa5d..8cf9712c65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -492,9 +492,6 @@ importers: effect: specifier: 'catalog:' version: 4.0.0-rc.112 - undici: - specifier: ^8.10.0 - version: 8.10.1 devDependencies: '@tsconfig/bun': specifier: 'catalog:'