Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions packages/typegen/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@ lives in supabase-js.
- `src/languages/dart.ts` -- the first out-of-process entry:
`dart run supabase_typegen --output -`. No `--schema` flag: the document on
stdin already holds the schemas the consumer introspected.
- `src/languages/swift.ts` -- `supabase-typegen --access-control <level>`, a
prebuilt binary from supabase-swift's releases (SDK-2199). It downloads the
release the nearest `Package.resolved` pins into the user cache, then falls
back to PATH; `SUPABASE_TYPEGEN` overrides both. The only entry that touches
the network or the file system; `createSwift` takes `fetch` and the platform
so tests stay offline. `swift-access-control` maps to `--access-control` and
accepts only `internal` and `public`. Exit `65` is a rejected document.
- `src/languages/swift.ts` -- `supabase-typegen --access-control <level>`, built
from source (SDK-3043) at the supabase-swift commit the nearest
`Package.resolved` pins, with the user's `git` and `swift`, and cached per
commit; then falls back to PATH; `SUPABASE_TYPEGEN` overrides both. Before
fetching it proves the commit is upstream's (tag for a version pin, ancestry
of the branch otherwise), since GitHub serves fork commits by SHA. The build
steps run through `host.spawn` with a temporary folder as `cwd`. The only
entry that touches the network or the file system; `createSwift` takes the
platform, the temporary directory and the progress `log` so tests stay
hermetic. `swift-access-control` maps to `--access-control` and accepts only
`internal` and `public`. Exit `65` is a rejected document.
- `src/languages/index.ts` -- the three in-process entries and the `languages`
list. `typescript` has no user options; `detect-one-to-one-relationships`
(default true; the CLI's deprecated `--postgrest-v9-compat` sets it false),
Expand Down Expand Up @@ -107,5 +111,6 @@ with `parseGeneratorMetadata`. `test/in-process.test.ts` asserts each bundled
language's output equals the direct generator call on sorted input (the
byte-identity acceptance criterion, without Docker). `test/dart.test.ts` and
`test/swift.test.ts` cover the handoff and every error classification with the
fake host from `test/helpers.ts`; `test/node-host.test.ts` runs a fake `dart`
shell script on PATH.
fake host from `test/helpers.ts`. `test/swift.test.ts` also runs the source
build through `createNodeHost` against fake `git` and `swift` shell scripts on
PATH; `test/node-host.test.ts` runs a fake `dart` shell script on PATH.
58 changes: 47 additions & 11 deletions packages/typegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,63 @@ not.
| `typescript` | in-process | `generateTypescript` from `@supabase/postgrest-typegen` | consumer: `detect-one-to-one-relationships`, `postgrest-version`, `default-schema` |
| `go` | in-process | `generateGo` from `@supabase/postgrest-typegen` | |
| `python` | in-process | `generatePython` from `@supabase/postgrest-typegen` | |
| `swift` | out-of-process | `supabase-typegen --access-control <level>`, fetched | `--swift-access-control internal\|public` |
| `swift` | out-of-process | `supabase-typegen --access-control <level>`, built | `--swift-access-control internal\|public` |
| `dart` | out-of-process | `dart run supabase_typegen --output -` in the project | |

The three in-process entries are a transition: as each generator relocates to
its SDK repository (SDK-1641), its entry here changes to an out-of-process
command, or for TypeScript to an import of the new npm package, and consumers
notice nothing but a dependency bump.

Swift moved first. `supabase-typegen` is a prebuilt executable from the
supabase-swift releases, not a package dependency: as a SwiftPM product its
Swift moved first. `supabase-typegen` is an executable in supabase-swift's
`tools/supabase-typegen`, not a package dependency: as a SwiftPM product its
swift-format dependency would pin every app's swift-syntax, and an Xcode project
without a `Package.swift` has nothing to `swift run` from.

The output targets one version's `@Table` macro, so the binary must match the
project's supabase-swift. The entry reads that version from the nearest
`Package.resolved` (in `cwd` or a parent; SwiftPM's, or Xcode's inside the
`.xcodeproj` or `.xcworkspace`), downloads that release's asset once into
`$XDG_CACHE_HOME/supabase/typegen/<version>/` (default `~/Library/Caches` on
macOS, `~/.cache` elsewhere) and runs it. Without a version pin, or on a
platform with no asset (anything but macOS and Linux x86_64), it runs
`supabase-typegen` from PATH. `SUPABASE_TYPEGEN=<path>` overrides both.
The output targets one commit's `@Table` macro, so the generator must match the
project's supabase-swift. The entry reads the supabase-swift pin from the
nearest `Package.resolved` (in `cwd` or a parent; SwiftPM's, or Xcode's inside
the `.xcodeproj` or `.xcworkspace`) and builds the generator from source at the
pin's `revision`, so version, branch and revision pins all work:

1. The revision must be a 40-character lowercase hex SHA, a version must be a
semantic version and a branch a plain branch name; nothing else reaches
`git`. Only regular files the user owns count as `Package.resolved`, so
another user's file in a shared parent such as `/tmp` cannot pick the commit,
and a malformed one counts as no pin.
2. A binary already in `$XDG_CACHE_HOME/supabase/typegen/<revision>/` (default
`~/Library/Caches` on macOS, `~/.cache` elsewhere; an empty or relative
`XDG_CACHE_HOME` is ignored) runs with no build.
3. Otherwise it checks that `git` and `swift` (Swift 6.2 / Xcode 26 or newer)
are on PATH and that the commit is upstream's. GitHub serves every commit of
the fork network through the upstream URL, pull request heads included, so a
version pin's tag (`<version>` or `v<version>`, per `git ls-remote`) must
point at the revision, and a branch pin's revision must be an ancestor of
that upstream branch (a bare revision pin: the default branch), checked in a
commits-only clone (`--filter=tree:0`, about 1.5 MB and a second). It then
fetches the single commit into a temporary folder, checks that `HEAD` is the
revision, and runs
`swift build -c release --package-path tools/supabase-typegen --disable-automatic-resolution`.
The flag builds the generator's committed `Package.resolved` exactly and
fails rather than resolve other dependency versions. Linux adds
`--static-swift-stdlib`, so the cached binary keeps working after the
toolchain that built it is removed or upgraded. Git's repository variables
(`GIT_DIR`, `GIT_INDEX_FILE` and the rest of
`git rev-parse --local-env-vars`) are removed from these steps, so running
inside a git hook cannot touch the user's repository.
4. For a version pin, `supabase-typegen --version` must print that version.
5. The binary is copied beside its cache path and renamed into place, so
concurrent runs are safe. The temporary folder is removed in every case.

The first build prints a line to stderr and takes a few minutes, mostly
compiling swift-syntax: about 5.5 minutes on an Apple M3 with Swift 6.4, and
about the same on Linux aarch64 with Swift 6.2 in Docker on that machine. It
trusts nothing the app does not already run: the supabase-swift source at the
pinned commit, the generator's pinned dependencies and the user's toolchain.
supabase-swift 2.x has no `tools/supabase-typegen` and fails with a request to
update to 3.0.0-alpha.1 or newer. Without a supabase-swift pin, or on Windows,
the entry runs `supabase-typegen` from PATH. `SUPABASE_TYPEGEN=<path>` overrides
both.

`--swift-access-control` lost `private` and `package`, which the new generator
does not accept.
Expand Down
5 changes: 4 additions & 1 deletion packages/typegen/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export interface SpawnRequest {
/** Executable name, resolved against `PATH` by the host (for example `dart`). */
readonly command: string;
readonly args: readonly string[];
/** Directory to run in; always the host's `cwd`, the user's project. */
/**
* Directory to run in: the host's `cwd`, the user's project, for a
* generator, or a directory the language entry chose for a setup step.
*/
readonly cwd: string;
readonly env: Readonly<Record<string, string | undefined>>;
/** Complete document to write to the child's stdin before closing it. */
Expand Down
5 changes: 4 additions & 1 deletion packages/typegen/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export class SpawnUnavailableError extends TypegenError {
}
}

/** An out-of-process generator exited unsuccessfully. */
/**
* An out-of-process generator, or a step that builds one, exited
* unsuccessfully.
*/
export class ToolFailedError extends TypegenError {
readonly command: readonly string[];
readonly exitCode: number | null;
Expand Down
3 changes: 2 additions & 1 deletion packages/typegen/src/languages/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface ExternalTool {
classify?(result: SpawnResult): ToolFailure | undefined;
}

const isCommandNotFound = (error: unknown): boolean =>
/** Whether `Host.spawn` rejected because the command is not on PATH. */
export const isCommandNotFound = (error: unknown): boolean =>
typeof error === "object" &&
error !== null &&
"code" in error &&
Expand Down
Loading
Loading