Skip to content
Open
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
32 changes: 23 additions & 9 deletions packages/typegen/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,26 @@ 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 a supabase-swift commit picked in this order
(SDK-3045): the `swift-typegen-source` option (a local checkout starting with
`./`, `../`, `/` or `~/`, built in place and never cached; a full SHA; a
version, resolved through its tag; a branch, resolved on every run, all with
`git ls-remote`), the supabase-swift pin of the nearest `Package.resolved`
that has one (`cwd` and parents, then up to two levels down, where the pins
must agree), or the latest 3.x release with a notice. Each run logs the commit
it uses. The binary is cached per commit. There is no PATH fallback and no
`SUPABASE_TYPEGEN`; Windows is refused. Before fetching a commit that did not
come from upstream's own refs 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 value parsing, `git ls-remote` parsing and semver
selection are pure exported functions (`parseTypegenSource`, `parseRefs`,
`compareVersions`, `latestRelease`) with direct tests. 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 +120,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.
105 changes: 94 additions & 11 deletions packages/typegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,110 @@ 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`, `--swift-typegen-source <source>` |
| `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 builds it from source at a supabase-swift
commit, which it picks in this order:

1. `--swift-typegen-source`, when given. The value is read in a fixed order,
without guessing:
- A value that starts with `./`, `../`, `/` or `~/` is a local checkout.
Relative paths must start with `./` or `../`, so a folder named `3.0.0` is
never read as a version, and `~` means the home folder. The entry checks
that the checkout has `tools/supabase-typegen`, runs
`swift build -c release --package-path <checkout>/tools/supabase-typegen`
in the checkout's own `.build` folder, so later builds are incremental, and
runs the binary from there. It never caches it, since the working tree can
hold uncommitted changes. This is the form for working on the generator, or
for a fork.
- 40 hex characters are a commit SHA. An abbreviated SHA is refused.
- A semantic version, with or without a leading `v`, is the commit its tag
`v<version>` (or `<version>`) points at, per `git ls-remote`; an annotated
tag's commit wins. A version below 3 is refused, since supabase-swift 2.x
has no generator.
- Anything else is a branch, resolved to its commit with `git ls-remote` on
every run, so new commits are picked up. A branch name that starts with `-`
or holds whitespace, `..`, control characters or anything else git could
read as an option or a refspec is refused before git runs.

A lookup that finds nothing names what it looked for, and for a branch name
that is also a folder in `cwd`, the `./` form to build it.

2. The supabase-swift pin of the nearest `Package.resolved` that has one: in
`cwd` or a parent, SwiftPM's or Xcode's inside the `.xcodeproj` or
`.xcworkspace`. Lockfiles without a supabase-swift pin are skipped. For a
monorepo run from its root, when no parent has a pin, the entry also looks up
to two levels below `cwd`, skipping `.git`, `.build`, `node_modules`, `Pods`
and `DerivedData`; the pins found there must agree on the revision, or the
run fails and asks for `--swift-typegen-source`. The pin's `revision` is
built, so version, branch and revision pins all work.
3. The latest supabase-swift 3.x release from `git ls-remote --tags`: the
highest stable 3.x once there is one, the highest 3.x prerelease before that
(with semantic version precedence, so `alpha.10` follows `alpha.9`), and
never another major version. A notice on stderr says so and asks to run from
the app's directory, or to pass `--swift-typegen-source`, to match the pinned
version.

Every run prints the commit it uses to stderr, for example
``Using branch `main` at `4f2a9c1…`.``. Except for a local checkout, the build
then goes:

1. Only a 40-character lowercase hex SHA reaches `git` as a revision, a version
must be a semantic version and a branch a plain branch name. 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, for a commit that did not come from upstream's own tags or
branches, that it 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, or a SHA passed to
`--swift-typegen-source`: 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, from the pin, the flag or the latest release,
`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. A
pinned build 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. A version or the latest release trusts the tag at the time of the
run, and a branch anyone who can push to it, which is why a branch is never a
default. supabase-swift 2.x has no `tools/supabase-typegen` and fails with a
request to update to 3.0.0-alpha.1 or newer. The entry never runs a
`supabase-typegen` from PATH, and `SUPABASE_TYPEGEN` is gone: a stale binary
could generate code for the wrong version without a warning, and a local
checkout covers the development loop. Windows is not supported.

`--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