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
16 changes: 16 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm exec turbo run build --filter='./rivetkit-typescript/packages/*'
- run: pnpm exec turbo run check-types --filter='./rivetkit-typescript/packages/*'
rivetkit-edge-budget:
name: RivetKit / Edge Bundle Budget
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec turbo run build --filter=@rivetkit/supabase
# Blocks the native dependency closure from re-entering the edge adapter.
- run: pnpm --filter @rivetkit/supabase check-edge-closure
# Budgets the adapter's bundled JS size.
- run: pnpm --filter @rivetkit/supabase size
# website-type-check:
# name: Website / Type Check
# runs-on: ubuntu-latest
Expand Down
56 changes: 53 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,13 @@ export async function loadNapiRuntime(): Promise<{
bindings: NapiBindings;
runtime: NapiCoreRuntime;
}> {
// LOAD-BEARING: the specifier is computed (`.join("/")`) on purpose. Do NOT
// "simplify" it to a literal `import("@rivetkit/rivetkit-napi")`. Edge
// adapters (e.g. @rivetkit/supabase) pre-bundle this module for Deno; a
// literal dynamic import is statically resolvable, so Deno's eszip bundler
// would snapshot the native `.node` addon into the deploy and 413. The
// computed specifier keeps it opaque to static analysis so it is never
// bundled. Enforced by scripts/ci/check-edge-native-closure.mjs.
const bindings = await import(["@rivetkit", "rivetkit-napi"].join("/"));
return {
bindings,
Expand Down
7 changes: 7 additions & 0 deletions rivetkit-typescript/packages/rivetkit/src/registry/native.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VirtualWebSocket } from "@rivetkit/virtual-websocket";

Check failure on line 1 in rivetkit-typescript/packages/rivetkit/src/registry/native.ts

View workflow job for this annotation

GitHub Actions / RivetKit / Quality Check

format

Formatter would have printed the following content:
import {
ACTOR_CONTEXT_INTERNAL_SYMBOL,
CONN_STATE_MANAGER_SYMBOL,
Expand Down Expand Up @@ -622,6 +622,13 @@
}

async function loadEngineCli(): Promise<typeof import("@rivetkit/engine-cli")> {
// LOAD-BEARING: the specifier is computed (`.join("/")`) on purpose. Do NOT
// "simplify" it to a literal `import("@rivetkit/engine-cli")`. Edge adapters
// (e.g. @rivetkit/supabase) pre-bundle this module for Deno; a literal
// dynamic import is statically resolvable, so Deno's eszip bundler would
// snapshot the ~117 MB engine-cli binary into the deploy and 413. The
// computed specifier keeps it opaque to static analysis so it is never
// bundled. Enforced by scripts/ci/check-edge-native-closure.mjs.
return import(["@rivetkit", "engine-cli"].join("/"));
}

Expand Down
9 changes: 9 additions & 0 deletions rivetkit-typescript/packages/supabase/.size-limit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"name": "supabase adapter (bundled, raw)",
"path": "dist/mod.mjs",
"limit": "1.3 MB",
"gzip": false,
"brotli": false
}
]
10 changes: 8 additions & 2 deletions rivetkit-typescript/packages/supabase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@
},
"scripts": {
"build": "tsup src/mod.ts",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"size": "size-limit",
"check-edge-closure": "node ../../../scripts/ci/check-edge-native-closure.mjs"
},
"dependencies": {
"@rivetkit/rivetkit-wasm": "workspace:*",
"rivetkit": "workspace:^"
"cbor-x": "^1.6.0",
"pino": "^9.5.0"
},
"devDependencies": {
"@size-limit/file": "^11.1.6",
"@types/node": "^22.0.0",
"rivetkit": "workspace:^",
"size-limit": "^11.1.6",
"tsup": "^8.4.0",
"typescript": "^5.5.2"
},
Expand Down
9 changes: 9 additions & 0 deletions rivetkit-typescript/packages/supabase/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import {
setup as rivetkitSetup,
} from "rivetkit";

// Re-export the rivetkit authoring API (actor, createClient helpers, types,
// etc.) so a Supabase Edge Function's import map can point `rivetkit` at this
// pre-bundled adapter. The user's source still reads `import { actor } from
// "rivetkit"`; the import map redirects it here, so the deploy never pulls
// rivetkit's native dependency closure into the Deno eszip. The local `setup`
// and `serve` below intentionally shadow rivetkit's `setup`, wiring the wasm
// runtime automatically.
export * from "rivetkit";

const DEFAULT_MANAGER_PATH = "/api/rivet";

/** Config passed to `setup` / `serve`. The wasm runtime is wired automatically. */
Expand Down
60 changes: 58 additions & 2 deletions rivetkit-typescript/packages/supabase/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,60 @@
import { builtinModules } from "node:module";
import { defineConfig } from "tsup";
import defaultConfig from "../../../tsup.base.ts";

export default defineConfig(defaultConfig);
// Pre-bundle the rivetkit wasm-path runtime into this adapter's own dist. A
// Supabase Edge Function deploy (Deno eszip) snapshots the entire declared npm
// dependency closure, so a package that declares `rivetkit` as a runtime
// dependency drags in rivetkit's native packages (engine-cli, rivetkit-napi,
// agent-os secure-exec) that the wasm runtime never executes. By bundling
// rivetkit here and not declaring it as a runtime dependency, and by having the
// Supabase function's import map point `rivetkit` at this adapter, the deploy
// ships only the code the wasm path actually uses. Only @rivetkit/rivetkit-wasm
// and a few node-oriented CJS libs stay external (see below).
export default defineConfig({
entry: { mod: "src/mod.ts" },
outDir: "dist",
target: "esnext",
platform: "node",
format: ["esm", "cjs"],
sourcemap: false,
clean: true,
dts: {
compilerOptions: {
skipLibCheck: true,
resolveJsonModule: true,
},
},
splitting: false,
skipNodeModulesBundle: false,
shims: false,
external: [
"@rivetkit/rivetkit-wasm",
// Native packages the wasm path never executes.
"@rivetkit/rivetkit-napi",
"@rivetkit/engine-cli",
"@rivet-dev/agent-os-core",
// Node CommonJS libs with dynamic require() that esbuild cannot bundle
// into ESM for Deno; Deno's node compat loads them at runtime. Declared
// as runtime dependencies of this package.
"pino",
"cbor-x",
],
esbuildPlugins: [
{
// Deno requires the `node:` prefix on built-in modules. esbuild, when
// bundling node-targeted CJS deps, can emit bare `import "module"` /
// `import "os"` which Deno rejects. Rewrite every bare Node built-in
// import to its `node:`-prefixed external form.
name: "node-builtin-prefix",
setup(build) {
build.onResolve({ filter: /^[^.]/ }, (args) => {
const bare = args.path.replace(/^node:/, "");
if (builtinModules.includes(bare)) {
return { path: `node:${bare}`, external: true };
}
return undefined;
});
},
},
],
});
111 changes: 111 additions & 0 deletions scripts/ci/check-edge-native-closure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Guard: keep the Supabase edge adapter free of the native packages the wasm
// runtime never uses (`@rivetkit/rivetkit-napi`, `@rivetkit/engine-cli`,
// `@rivet-dev/agent-os-core`). A Supabase Edge Function deploy (Deno eszip)
// snapshots the whole declared npm closure AND statically resolves literal
// dynamic imports, so either can silently re-bloat the deploy by hundreds of MB
// and 413.
//
// Two independent regressions, two checks:
// 1. Dependency closure: a forbidden package re-enters the adapter's prod
// dependency tree (directly or transitively).
// 2. Literal import in the bundle: rivetkit-core loads these via a computed
// specifier `import(["@rivetkit","rivetkit-napi"].join("/"))` that esbuild
// and Deno cannot statically resolve. If someone "simplifies" that to a
// literal `import("@rivetkit/rivetkit-napi")`, the adapter's pre-bundled
// dist would contain a statically-resolvable specifier that Deno's eszip
// snapshots. size-limit and the closure check both miss this (the literal
// import is external, so it neither inflates the bundle nor changes any
// `dependencies` field), so we grep the built bundle directly.
//
// Exits 1 if either check fails.
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const ADAPTER = "@rivetkit/supabase";
const FORBIDDEN = [
"@rivetkit/rivetkit-napi",
"@rivetkit/engine-cli",
"@rivet-dev/agent-os-core",
];

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
let failed = false;

// --- Check 1: production dependency closure ---
{
const raw = execFileSync(
"pnpm",
["--filter", ADAPTER, "list", "--prod", "--depth", "Infinity", "--json"],
{ encoding: "utf8" },
);
const found = new Set();
const walk = (deps) => {
if (!deps) return;
for (const [name, info] of Object.entries(deps)) {
if (FORBIDDEN.includes(name)) found.add(name);
walk(info.dependencies);
}
};
for (const project of JSON.parse(raw)) {
walk(project.dependencies);
walk(project.devDependencies);
walk(project.optionalDependencies);
}
if (found.size > 0) {
failed = true;
for (const name of found) {
console.error(
`::error::forbidden native package '${name}' is in ${ADAPTER}'s production closure; ` +
"a Supabase edge deploy would embed it and 413. Keep it out of the adapter's deps.",
);
}
} else {
console.log(`ok: ${ADAPTER} edge closure is free of native packages`);
}
}

// --- Check 2: no literal native import specifiers in the built bundle ---
{
const bundles = [
resolve(repoRoot, "rivetkit-typescript/packages/supabase/dist/mod.mjs"),
resolve(repoRoot, "rivetkit-typescript/packages/supabase/dist/mod.js"),
].filter(existsSync);

if (bundles.length === 0) {
console.error(
`::error::${ADAPTER} dist not built; run \`pnpm --filter ${ADAPTER} build\` before this check`,
);
failed = true;
}

for (const file of bundles) {
const src = readFileSync(file, "utf8");
for (const pkg of FORBIDDEN) {
const p = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// A literal module specifier for a forbidden package inside
// import(...) / require(...) / `from "..."`. The load-bearing computed
// form `["@rivetkit","rivetkit-napi"].join("/")` does not match (the
// full specifier never appears as one quoted string), and error-message
// strings that merely mention the name are not specifiers.
const re = new RegExp(
`(?:\\bimport\\s*\\(|\\brequire\\s*\\(|\\bfrom)\\s*['"\`]${p}['"\`]`,
);
if (re.test(src)) {
failed = true;
console.error(
`::error::${file.split("/").pop()} contains a literal import of '${pkg}'. ` +
"rivetkit-core must load native packages via the computed " +
'`import([...].join("/"))` form so Deno cannot statically snapshot them. ' +
"Do not simplify it to a literal specifier.",
);
}
}
}
if (bundles.length > 0 && !failed) {
console.log(`ok: ${ADAPTER} bundle has no literal native import specifiers`);
}
}

process.exit(failed ? 1 : 0);
Loading
Loading