Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,14 @@ describe("legacy functions serve integration", () => {
expect(dockerRun.args).toContain("--add-host");
expect(dockerRun.args).toContain("host.docker.internal:host-gateway");
expect(dockerRun.args).toContain("public.ecr.aws/supabase/edge-runtime:v1.73.13");
expect(
extractFlagValues(dockerRun.args, "-v").some((value) =>
value.endsWith(":/root/index.ts:ro"),
),
).toBe(true);
expect(dockerRun.args[dockerRun.args.length - 1]).toBe(
"edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
);

const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun));
expect(envs).toContain("HELLO=WORLD");
Expand Down Expand Up @@ -1147,7 +1155,14 @@ describe("legacy functions serve integration", () => {
}

const commandScript = dockerRun.args[dockerRun.args.length - 1] ?? "";
expect(commandScript).toContain("cat <<'EOF' > /root/index.ts");
expect(commandScript).toBe(
"edge-runtime start --main-service=/root --port=8081 --policy=per_worker\n",
);
expect(
extractFlagValues(dockerRun.args, "-v").some((value) =>
value.endsWith(":/root/index.ts:ro"),
),
).toBe(true);
expect(commandScript).not.toContain("@ts-nocheck");
expect(commandScript).not.toContain("declare const Deno");
expect(commandScript).not.toContain("declare const EdgeRuntime");
Expand Down
64 changes: 29 additions & 35 deletions apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const dockerLogDiagnosticTailLength = 4_096;
const remoteJwksTimeoutMs = 10_000;
const legacyDefaultEdgeRuntimeVersion = "v1.74.1";
const defaultSupabaseEnv = "development";
const serveMainContainerPath = "/root/index.ts";
const clerkDomainPattern = /^(clerk([.][a-z0-9-]+){2,}|([a-z0-9-]+[.])+clerk[.]accounts[.]dev)$/;
const shellVariableNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
let cachedLegacyFunctionsServeMainTemplate: string | undefined;
Expand Down Expand Up @@ -1229,30 +1230,26 @@ const writeStoppedServingMessage = Effect.fnUntraced(function* () {
yield* output.raw(`Stopped serving ${styleText("bold", functionsDirName)}\n`, "stdout");
});

// The Go CLI writes the runtime template to /root/index.ts via a quoted `<<'EOF'`
// heredoc; we keep the same terminator for byte-parity with its entrypoint. A line
// equal to the terminator inside the template would close the heredoc early and
// silently corrupt the script, so fail loudly instead. `serve.main.ts` (the only
// template) is asserted to contain no such line by a unit test.
const serveEntrypointHeredocTerminator = "EOF";

export function buildServeEntrypointScript(
template: string,
export function buildServeEntrypointCommand(
command: ReadonlyArray<string>,
multilineEnvScriptPath?: string,
) {
if (template.split("\n").includes(serveEntrypointHeredocTerminator)) {
throw new Error(
`functions serve runtime template contains a line equal to the heredoc terminator "${serveEntrypointHeredocTerminator}"`,
);
}
return `cat <<'${serveEntrypointHeredocTerminator}' > /root/index.ts
${template}
${serveEntrypointHeredocTerminator}
${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")}
return `${multilineEnvScriptPath === undefined ? "" : `. ${multilineEnvScriptPath}\n`}${command.join(" ")}
`;
}

async function writeServeMainTemplateFile(template: string) {
// Mount the bundled runtime template instead of embedding it in `sh -c` so
// Windows does not hit `uv_spawn` ENAMETOOLONG on path-heavy projects.
const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-main-"));
const pathname = join(dir, "index.ts");
await writeFile(pathname, template);
return {
bind: `${pathname}:${serveMainContainerPath}:ro`,
cleanup: () => rm(dir, { recursive: true, force: true }),
} as const;
}

function edgeRuntimeImageTag(version: string) {
return version.startsWith("v") ? version : `v${version}`;
}
Expand Down Expand Up @@ -1420,6 +1417,9 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: {
...(input.debug ? ["--verbose"] : []),
];
const serveMainTemplate = yield* Effect.promise(() => getLegacyFunctionsServeMainTemplate());
const serveMainTemplateFile = yield* Effect.tryPromise(() =>
writeServeMainTemplateFile(serveMainTemplate),
).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))));
const command = [
"run",
"-d",
Expand All @@ -1437,6 +1437,8 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: {
`com.supabase.cli.project=${labels["com.supabase.cli.project"]}`,
"--label",
`com.docker.compose.project=${labels["com.docker.compose.project"]}`,
"-v",
serveMainTemplateFile.bind,
...([...binds] as ReadonlyArray<string>).flatMap((bind) => ["-v", bind]),
...(dockerMultilineEnvScript === undefined ? [] : ["-v", dockerMultilineEnvScript.bind]),
...(dockerEnvFile === undefined ? [] : ["--env-file", dockerEnvFile.path]),
Expand All @@ -1450,26 +1452,18 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: {
"sh",
legacyGetRegistryImageUrl(`supabase/edge-runtime:${edgeRuntimeImageTag(edgeRuntimeVersion)}`),
"-c",
buildServeEntrypointScript(
serveMainTemplate,
runtimeCommand,
dockerMultilineEnvScript?.scriptPath,
),
buildServeEntrypointCommand(runtimeCommand, dockerMultilineEnvScript?.scriptPath),
];

const cleanupRuntimeArtifacts =
const cleanupRuntimeArtifacts = Effect.all([
Effect.tryPromise(() => serveMainTemplateFile.cleanup()).pipe(Effect.orDie),
dockerEnvFile === undefined
? dockerMultilineEnvScript === undefined
? Effect.void
: Effect.tryPromise(() => dockerMultilineEnvScript.cleanup()).pipe(Effect.orDie)
: Effect.tryPromise(() => dockerEnvFile.cleanup()).pipe(
Effect.andThen(
dockerMultilineEnvScript === undefined
? Effect.void
: Effect.tryPromise(() => dockerMultilineEnvScript.cleanup()).pipe(Effect.orDie),
),
Effect.orDie,
);
? Effect.void
: Effect.tryPromise(() => dockerEnvFile.cleanup()).pipe(Effect.orDie),
dockerMultilineEnvScript === undefined
? Effect.void
: Effect.tryPromise(() => dockerMultilineEnvScript.cleanup()).pipe(Effect.orDie),
]).pipe(Effect.asVoid);

return yield* Effect.gen(function* () {
yield* output.raw("Setting up Edge Functions runtime...\n", "stderr");
Expand Down
32 changes: 11 additions & 21 deletions apps/cli/src/shared/functions/serve.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import { describe, expect, it } from "vitest";

import { bundleServeMainTemplate } from "./serve-main-bundler.ts";
import { buildServeEntrypointScript } from "./serve.ts";
import { buildServeEntrypointCommand } from "./serve.ts";

describe("buildServeEntrypointScript", () => {
const template = ['import { x } from "y";', "Deno.serve(() => new Response());"].join("\n");

it("writes the template through the heredoc and appends the runtime command", () => {
const script = buildServeEntrypointScript(template, ["edge-runtime", "start"]);
expect(script).toContain("cat <<'EOF' > /root/index.ts");
expect(script).toContain(template);
expect(script).toContain("edge-runtime start");
expect(script).not.toContain(". /");
describe("buildServeEntrypointCommand", () => {
it("returns the runtime command without embedding the template body", () => {
const script = buildServeEntrypointCommand(["edge-runtime", "start"]);
expect(script).toBe("edge-runtime start\n");
expect(script).not.toContain("Deno.serve");
});

it("sources the multiline env script before the runtime command when provided", () => {
const script = buildServeEntrypointScript(template, ["edge-runtime", "start"], "/root/env.sh");
const script = buildServeEntrypointCommand(["edge-runtime", "start"], "/root/env.sh");
expect(script).toContain(". /root/env.sh\nedge-runtime start");
});

it("fails loudly when the template contains a bare heredoc terminator line", () => {
const poisoned = ["line-1", "EOF", "line-3"].join("\n");
expect(() => buildServeEntrypointScript(poisoned, ["edge-runtime", "start"])).toThrow(
'heredoc terminator "EOF"',
);
});

it("does not let the real bundled serve.main.ts template close the heredoc early", async () => {
it("keeps the spawned command short even with the real bundled template", async () => {
const bundled = await bundleServeMainTemplate();
expect(bundled.split("\n")).not.toContain("EOF");
expect(() => buildServeEntrypointScript(bundled, ["edge-runtime", "start"])).not.toThrow();
const script = buildServeEntrypointCommand(["edge-runtime", "start"]);
expect(bundled.length).toBeGreaterThan(20_000);
expect(script.length).toBeLessThan(128);
});
});
Loading