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
2 changes: 1 addition & 1 deletion apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ Legend:
| `functions download` | `ported` | [`../src/legacy/commands/functions/download/download.command.ts`](../src/legacy/commands/functions/download/download.command.ts) |
| `functions deploy` | `ported` | [`../src/legacy/commands/functions/deploy/deploy.command.ts`](../src/legacy/commands/functions/deploy/deploy.command.ts) |
| `functions new` | `wrapped` | [`../src/legacy/commands/functions/new/new.command.ts`](../src/legacy/commands/functions/new/new.command.ts) |
| `functions serve` | `wrapped` | [`../src/legacy/commands/functions/serve/serve.command.ts`](../src/legacy/commands/functions/serve/serve.command.ts) |
| `functions serve` | `ported` | [`../src/legacy/commands/functions/serve/serve.command.ts`](../src/legacy/commands/functions/serve/serve.command.ts) |
| `storage ls` | `wrapped` | [`../src/legacy/commands/storage/ls/ls.command.ts`](../src/legacy/commands/storage/ls/ls.command.ts) |
| `storage cp` | `wrapped` | [`../src/legacy/commands/storage/cp/cp.command.ts`](../src/legacy/commands/storage/cp/cp.command.ts) |
| `storage mv` | `wrapped` | [`../src/legacy/commands/storage/mv/mv.command.ts`](../src/legacy/commands/storage/mv/mv.command.ts) |
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
"ignore": [
"scripts/*.ts",
"tests/**/*.ts",
"src/shared/telemetry/event-catalog.ts"
"src/shared/telemetry/event-catalog.ts",
"src/shared/functions/serve.main.ts"
],
"ignoreBinaries": [
"nx"
Expand Down
27 changes: 16 additions & 11 deletions apps/cli/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ const { values } = parseArgs({
},
});

const version = values.version;
if (!version) {
console.error(
"Usage: pnpm exec bun apps/cli/scripts/build.ts --version <npm-version> --shell <legacy|next>",
);
process.exit(1);
}

const shell = values.shell;
if (shell !== "legacy" && shell !== "next") {
console.error(`Invalid --shell value: ${String(shell)}. Expected "legacy" or "next".`);
process.exit(1);
}
const root = path.resolve(import.meta.dir, "../../..");
const packageJsonPath = path.join(root, "apps/cli/package.json");
const packageVersion = JSON.parse(await readFile(packageJsonPath, "utf8")) as { version?: string };
const version = values.version ?? packageVersion.version;
if (!version) {
console.error(
"Usage: pnpm exec bun apps/cli/scripts/build.ts [--version <npm-version>] --shell <legacy|next>",
);
process.exit(1);
}

const TARGETS = [
{
Expand Down Expand Up @@ -82,10 +84,13 @@ const TARGETS = [
},
] as const;

const root = path.resolve(import.meta.dir, "../../..");
const entrypoint = path.join(root, "apps/cli/src", shell, "main.ts");
const distDir = path.join(root, "dist");
const goSource = path.resolve(root, "apps/cli-go");
const serveMainTemplateSource = path.join(root, "apps/cli/src/shared/functions/serve.main.ts");
const serveMainTemplateDefine = `--define=SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE=${JSON.stringify(
await readFile(serveMainTemplateSource, "utf8"),
)}`;
const posthogBuildDefines = [
`--define=process.env.SUPABASE_CLI_POSTHOG_KEY=${JSON.stringify(process.env.POSTHOG_API_KEY ?? "")}`,
`--define=process.env.SUPABASE_CLI_POSTHOG_HOST=${JSON.stringify(process.env.POSTHOG_ENDPOINT ?? "")}`,
Expand Down Expand Up @@ -117,7 +122,7 @@ async function buildTarget(target: (typeof TARGETS)[number]) {
const libc = libcForBunTarget(target.bunTarget);

console.log(`[${target.pkg}] Compiling Bun CLI...`);
await $`bun build ${entrypoint} --compile --minify --target=${target.bunTarget} --define=process.env.SUPABASE_CLI_VERSION=${JSON.stringify(version)} --define=SUPABASE_LIBC=${JSON.stringify(libc)} ${posthogBuildDefines} --outfile=${outfile}`;
await $`bun build ${entrypoint} --compile --minify --target=${target.bunTarget} --define=process.env.SUPABASE_CLI_VERSION=${JSON.stringify(version)} --define=SUPABASE_LIBC=${JSON.stringify(libc)} ${serveMainTemplateDefine} ${posthogBuildDefines} --outfile=${outfile}`;
console.log(`[${target.pkg}] Done.`);
}

Expand Down Expand Up @@ -188,7 +193,7 @@ async function buildMuslBinaries() {
const outfile = path.join(binDir, "supabase");
const libc = libcForBunTarget(target.bunTarget);
console.log(`[${target.pkg}] Compiling Bun CLI (musl)...`);
await $`bun build ${entrypoint} --compile --minify --target=${target.bunTarget} --define=process.env.SUPABASE_CLI_VERSION=${JSON.stringify(version)} --define=SUPABASE_LIBC=${JSON.stringify(libc)} ${posthogBuildDefines} --outfile=${outfile}`;
await $`bun build ${entrypoint} --compile --minify --target=${target.bunTarget} --define=process.env.SUPABASE_CLI_VERSION=${JSON.stringify(version)} --define=SUPABASE_LIBC=${JSON.stringify(libc)} ${serveMainTemplateDefine} ${posthogBuildDefines} --outfile=${outfile}`;

if (shell === "legacy") {
// Go binary is CGO_ENABLED=0 (fully static), so the glibc Linux build works on
Expand Down
78 changes: 51 additions & 27 deletions apps/cli/src/legacy/commands/functions/serve/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,82 @@

## Files Read

| Path | Format | When |
| ---------------------------------------------- | ---------- | --------------------------------------------------- |
| `<workdir>/supabase/functions/<slug>/index.ts` | TypeScript | always (loads function source for serving) |
| `<workdir>/supabase/config.toml` | TOML | to resolve function config (verify_jwt, import_map) |
| `<env-file>` | plain text | when `--env-file` is set |
| Path | Format | When |
| -------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | on every startup / restart when the project config exists |
| `<workdir>/supabase/.temp/edge-runtime-version` | plain text | when present, to override the bundled edge-runtime image tag |
| `<workdir>/supabase/functions/.env` | dotenv | when `--env-file` is unset and the fallback env file exists |
| `<env-file>` | dotenv | when `--env-file` is set; relative paths resolve from the caller cwd |
| `<workdir>/supabase/functions/*/index.ts` | TypeScript | to discover filesystem-backed functions |
| config-declared entrypoints / import maps / static files and imports | mixed | for each enabled function while resolving Docker bind mounts |
| `<signing_keys_path>` | JSON | when `auth.signing_keys_path` is configured |
| `apps/cli/src/shared/functions/serve.main.ts` | TypeScript | as the CLI-owned worker bootstrap template source |

## Files Written

| Path | Format | When |
| ---- | ------ | ---- |
| — | — | — |
| Path | Format | When |
| ---------------------------- | ------ | --------------------------------------------- |
| `~/.supabase/telemetry.json` | JSON | always, at command exit via `Effect.ensuring` |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ---- | ---- | ------------ | ---------------------- |
| | — | — | — | — |
| `—` | `—` | `—` | `—` | `—` |

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ------------------------------------ | --------- |
| `SUPABASE_ACCESS_TOKEN` | auth token (for Deno KV remote mode) | no |
| Variable | Purpose | Required? |
| --------------------------------------------- | ---------------------------------------------------------- | ------------------------------------ |
| `SUPABASE_PROFILE` | resolves the legacy profile / API base URL | no (defaults to `supabase`) |
| `SUPABASE_WORKDIR` | overrides the project workdir | no (falls back to CLI cwd discovery) |
| `SUPABASE_PROJECT_ID` | legacy config-service override for project identity | no |
| env vars referenced by `supabase/config.toml` | config interpolation through `loadProjectEnvironment(...)` | no |
| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the edge-runtime Docker registry mirror | no (defaults to `public.ecr.aws`) |

## Exit Codes

| Code | Condition |
| ---- | --------------------------------- |
| `0` | server stopped (SIGINT/SIGTERM) |
| `1` | Docker not running or unavailable |
| `1` | function serve startup failure |
| Code | Condition |
| ---- | ---------------------------------------------------------------------- |
| `0` | clean shutdown after `SIGINT`, `SIGTERM`, or stdin close |
| `1` | Docker unavailable / `docker info` fails |
| `1` | local DB container is not running |
| `1` | invalid inspect flag combination or invalid project/auth config |
| `1` | env file, signing key, import map, or function bind resolution failure |
| `1` | edge-runtime container startup, log streaming, or restart loop failure |

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` |

## Output

### `--output-format text` (Go CLI compatible)

Prints startup information and live request logs as functions are invoked.
Writes lifecycle text to stderr / stdout while the command is running:

- `Setting up Edge Functions runtime...` before each container start
- `Skipped serving Function: <slug>` for disabled functions
- `File change detected: <path> (<op>)` when a watched file triggers a restart
- live `docker logs -f --timestamps` output from the edge-runtime container
- `Stopped serving supabase/functions` on clean shutdown

### `--output-format json`

Not applicable (proxied to Go binary).
Long-running raw log / error output only; there is no final success payload object for this command.

### `--output-format stream-json`

Not applicable (proxied to Go binary).
Long-running raw log / error events only; there is no terminal `result` event on success.

## Notes

- Serves all functions locally using Deno and the Supabase Edge Runtime (via Docker).
- `--no-verify-jwt` disables JWT verification for development.
- `--env-file` path to env file populated to Function environment.
- `--import-map` path to custom import map.
- `--inspect` / `--inspect-mode` activates Deno inspector for debugging.
- `--all` is a hidden flag (default true) retained for backward compatibility; it has no effect because the Go CLI always serves all functions.
- Phase 0 proxy: all invocations are forwarded to the bundled Go binary.
- The hidden `--all` flag is still parsed but ignored; the native port always serves every discovered function, matching the Go command.
- Each restart re-reads config, rebuilds per-function bind mounts, recreates the `supabase_edge_runtime_<project>` container, and best-effort reloads Kong afterwards.
- The command creates or reuses Docker resources derived from the resolved project id:
- container: `supabase_edge_runtime_<project>`
- named volume: `supabase_edge_runtime_<project>`
- network: `supabase_network_<project>` unless `--network-id` overrides it
- Inspector mode exposes the configured `edge_runtime.inspector_port` on the host and sets `SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC=0`, matching the Go serve path.
37 changes: 32 additions & 5 deletions apps/cli/src/legacy/commands/functions/serve/serve.command.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { Layer } from "effect";
import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts";
import {
FUNCTIONS_SERVE_INSPECT_MODES,
serveFileWatcherLayer,
type FunctionsServeFlags,
} from "../../../../shared/functions/serve.ts";
import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts";
import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts";
import { legacyFunctionsServe } from "./serve.handler.ts";

const INSPECT_MODES = ["run", "brk", "wait"] as const;
const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer));
const legacyFunctionsServeRuntimeLayer = Layer.mergeAll(
serveFileWatcherLayer,
cliConfig,
legacyDebugLoggerLayer,
legacyTelemetryStateLayer,
commandRuntimeLayer(["functions", "serve"]),
);

const config = {
noVerifyJwt: Flag.boolean("no-verify-jwt").pipe(
Flag.withDescription("Disable JWT verification for the Function."),
Flag.optional,
),
envFile: Flag.string("env-file").pipe(
Flag.withDescription("Path to an env file to be populated to the Function environment."),
Expand All @@ -17,7 +37,7 @@ const config = {
Flag.optional,
),
inspect: Flag.boolean("inspect").pipe(Flag.withDescription("Alias of --inspect-mode brk.")),
inspectMode: Flag.choice("inspect-mode", INSPECT_MODES).pipe(
inspectMode: Flag.choice("inspect-mode", FUNCTIONS_SERVE_INSPECT_MODES).pipe(
Flag.withDescription("Activate inspector capability for debugging."),
Flag.optional,
),
Expand All @@ -26,15 +46,22 @@ const config = {
),
all: Flag.boolean("all").pipe(
Flag.withDescription("Serve all Functions."),
Flag.optional,
Flag.withDefault(true),
Flag.withHidden,
),
} as const;

export type LegacyFunctionsServeFlags = CliCommand.Command.Config.Infer<typeof config>;
export type LegacyFunctionsServeFlags = CliCommand.Command.Config.Infer<typeof config> &
FunctionsServeFlags;

export const legacyFunctionsServeCommand = Command.make("serve", config).pipe(
Command.withDescription("Serve all Functions locally."),
Command.withShortDescription("Serve all Functions locally"),
Command.withHandler((flags) => legacyFunctionsServe(flags)),
Command.withHandler((flags) =>
legacyFunctionsServe(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyFunctionsServeRuntimeLayer),
);
45 changes: 32 additions & 13 deletions apps/cli/src/legacy/commands/functions/serve/serve.handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import { Effect, Option } from "effect";
import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts";
import type { LegacyFunctionsServeFlags } from "./serve.command.ts";
import { Effect } from "effect";
import { join } from "node:path";
import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { LegacyDebugFlag, LegacyNetworkIdFlag } from "../../../../shared/legacy/global-flags.ts";
import { RuntimeInfo } from "../../../../shared/runtime/runtime-info.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import {
buildFunctionsServeInspectArgs,
resolveFunctionsServeInspectMode,
serveFunctions,
type FunctionsServeFlags,
} from "../../../../shared/functions/serve.ts";

export type LegacyFunctionsServeFlags = FunctionsServeFlags;

export const legacyResolveFunctionsServeInspectMode = resolveFunctionsServeInspectMode;
export const legacyBuildFunctionsServeInspectArgs = buildFunctionsServeInspectArgs;

export const legacyFunctionsServe = Effect.fn("legacy.functions.serve")(function* (
flags: LegacyFunctionsServeFlags,
) {
const proxy = yield* LegacyGoProxy;
const args: string[] = ["functions", "serve"];
if (flags.noVerifyJwt) args.push("--no-verify-jwt");
if (Option.isSome(flags.envFile)) args.push("--env-file", flags.envFile.value);
if (Option.isSome(flags.importMap)) args.push("--import-map", flags.importMap.value);
if (flags.inspect) args.push("--inspect");
if (Option.isSome(flags.inspectMode)) args.push("--inspect-mode", flags.inspectMode.value);
if (flags.inspectMain) args.push("--inspect-main");
if (Option.isSome(flags.all)) args.push(`--all=${flags.all.value ? "true" : "false"}`);
yield* proxy.exec(args);
const cliConfig = yield* LegacyCliConfig;
const runtimeInfo = yield* RuntimeInfo;
const telemetryState = yield* LegacyTelemetryState;
const debug = yield* LegacyDebugFlag;
const networkId = yield* LegacyNetworkIdFlag;

yield* serveFunctions(flags, {
projectRoot: cliConfig.workdir,
supabaseDir: join(cliConfig.workdir, "supabase"),
flagCwd: runtimeInfo.cwd,
platform: runtimeInfo.platform,
debug,
networkId,
projectIdOverride: cliConfig.projectId,
}).pipe(Effect.ensuring(telemetryState.flush));
});
Loading
Loading