From 14a9ad2e9a1f715518f4298acf76236373f1526b Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:14:08 +0200 Subject: [PATCH] fix(supabase): pre-bundle rivetkit for import-map redirect (native-free edge deploy) + edge bundle-budget CI + docs; regen stale BARE codecs --- .github/workflows/frontend.yml | 16 +++ pnpm-lock.yaml | 56 ++++++++- .../rivetkit/src/registry/napi-runtime.ts | 7 ++ .../packages/rivetkit/src/registry/native.ts | 7 ++ .../packages/supabase/.size-limit.json | 9 ++ .../packages/supabase/package.json | 10 +- .../packages/supabase/src/mod.ts | 9 ++ .../packages/supabase/tsup.config.ts | 60 +++++++++- scripts/ci/check-edge-native-closure.mjs | 111 ++++++++++++++++++ .../docs/actors/quickstart/supabase.mdx | 13 ++ website/src/content/docs/deploy/supabase.mdx | 2 + 11 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 rivetkit-typescript/packages/supabase/.size-limit.json create mode 100644 scripts/ci/check-edge-native-closure.mjs diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index dc18bfca89..4e1c84972f 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c92b54f1c..5a4590bd6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3471,13 +3471,25 @@ importers: '@rivetkit/rivetkit-wasm': specifier: workspace:* version: link:../rivetkit-wasm - rivetkit: - specifier: workspace:* - version: link:../rivetkit + cbor-x: + specifier: ^1.6.0 + version: 1.6.4 + pino: + specifier: ^9.5.0 + version: 9.9.5 devDependencies: + '@size-limit/file': + specifier: ^11.1.6 + version: 11.2.0(size-limit@11.2.0) '@types/node': specifier: ^22.0.0 version: 22.19.15 + rivetkit: + specifier: workspace:* + version: link:../rivetkit + size-limit: + specifier: ^11.1.6 + version: 11.2.0 tsup: specifier: ^8.4.0 version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.15))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) @@ -8581,6 +8593,12 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@size-limit/file@11.2.0': + resolution: {integrity: sha512-OZHE3putEkQ/fgzz3Tp/0hSmfVo3wyTpOJSRNm6AmcwX4Nm9YtTfbQQ/hZRwbBFR23S7x2Sd9EbqYzngKwbRoA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + size-limit: 11.2.0 + '@smithy/config-resolver@4.4.13': resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} engines: {node: '>=18.0.0'} @@ -10540,6 +10558,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes-iec@3.1.1: + resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} + engines: {node: '>= 0.8'} + bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -14252,6 +14274,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanospinner@1.2.2: + resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==} + nanostores@1.2.0: resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==} engines: {node: ^20.0.0 || >=22.0.0} @@ -15974,6 +15999,11 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} hasBin: true + size-limit@11.2.0: + resolution: {integrity: sha512-2kpQq2DD/pRpx3Tal/qRW1SYwcIeQ0iq8li5CJHQgOC+FtPn2BVmuDtzUCgNnpCrbgtfEHqh+iWzxK+Tq6C+RQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + skin-tone@2.0.0: resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} engines: {node: '>=8'} @@ -23740,6 +23770,10 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@size-limit/file@11.2.0(size-limit@11.2.0)': + dependencies: + size-limit: 11.2.0 + '@smithy/config-resolver@4.4.13': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -26340,6 +26374,8 @@ snapshots: esbuild: 0.27.3 load-tsconfig: 0.2.5 + bytes-iec@3.1.1: {} + bytes@3.0.0: {} bytes@3.1.2: {} @@ -30834,6 +30870,10 @@ snapshots: nanoid@3.3.6: {} + nanospinner@1.2.2: + dependencies: + picocolors: 1.1.1 + nanostores@1.2.0: {} napi-build-utils@2.0.0: {} @@ -32873,6 +32913,16 @@ snapshots: arg: 5.0.2 sax: 1.4.4 + size-limit@11.2.0: + dependencies: + bytes-iec: 3.1.1 + chokidar: 4.0.3 + jiti: 2.6.1 + lilconfig: 3.1.3 + nanospinner: 1.2.2 + picocolors: 1.1.1 + tinyglobby: 0.2.15 + skin-tone@2.0.0: dependencies: unicode-emoji-modifier-base: 1.0.0 diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts b/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts index cf6bb594cd..8020ef0eb8 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts @@ -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, diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts index f121dda805..eb1da6f0dc 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts @@ -622,6 +622,13 @@ function decodeNativeKvValue( } async function loadEngineCli(): Promise { + // 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("/")); } diff --git a/rivetkit-typescript/packages/supabase/.size-limit.json b/rivetkit-typescript/packages/supabase/.size-limit.json new file mode 100644 index 0000000000..fb7f723f36 --- /dev/null +++ b/rivetkit-typescript/packages/supabase/.size-limit.json @@ -0,0 +1,9 @@ +[ + { + "name": "supabase adapter (bundled, raw)", + "path": "dist/mod.mjs", + "limit": "1.3 MB", + "gzip": false, + "brotli": false + } +] diff --git a/rivetkit-typescript/packages/supabase/package.json b/rivetkit-typescript/packages/supabase/package.json index d507a73d96..539aca72c5 100644 --- a/rivetkit-typescript/packages/supabase/package.json +++ b/rivetkit-typescript/packages/supabase/package.json @@ -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" }, diff --git a/rivetkit-typescript/packages/supabase/src/mod.ts b/rivetkit-typescript/packages/supabase/src/mod.ts index 4b880182c9..fe05357bc4 100644 --- a/rivetkit-typescript/packages/supabase/src/mod.ts +++ b/rivetkit-typescript/packages/supabase/src/mod.ts @@ -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. */ diff --git a/rivetkit-typescript/packages/supabase/tsup.config.ts b/rivetkit-typescript/packages/supabase/tsup.config.ts index f363b829fd..8a83b9b3cc 100644 --- a/rivetkit-typescript/packages/supabase/tsup.config.ts +++ b/rivetkit-typescript/packages/supabase/tsup.config.ts @@ -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; + }); + }, + }, + ], +}); diff --git a/scripts/ci/check-edge-native-closure.mjs b/scripts/ci/check-edge-native-closure.mjs new file mode 100644 index 0000000000..fad4bb65c3 --- /dev/null +++ b/scripts/ci/check-edge-native-closure.mjs @@ -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); diff --git a/website/src/content/docs/actors/quickstart/supabase.mdx b/website/src/content/docs/actors/quickstart/supabase.mdx index 893ce1c8ef..2cc4175162 100644 --- a/website/src/content/docs/actors/quickstart/supabase.mdx +++ b/website/src/content/docs/actors/quickstart/supabase.mdx @@ -60,6 +60,19 @@ export const registry = setup({ use: { counter } }); await serve(registry); ``` +Add a `deno.json` next to the function so the deploy bundles only the WebAssembly runtime. It points `rivetkit` at the pre-bundled `@rivetkit/supabase`, keeping the deploy small. Without it, the deploy pulls Rivet's native engine and 413s. + +```json supabase/functions/rivet/deno.json +{ + "imports": { + "rivetkit": "npm:@rivetkit/supabase", + "@rivetkit/supabase": "npm:@rivetkit/supabase" + } +} +``` + +Your function code keeps importing from `rivetkit` as usual. The import map only changes how Deno resolves it at bundle time. + diff --git a/website/src/content/docs/deploy/supabase.mdx b/website/src/content/docs/deploy/supabase.mdx index 1423945449..768d87881b 100644 --- a/website/src/content/docs/deploy/supabase.mdx +++ b/website/src/content/docs/deploy/supabase.mdx @@ -33,6 +33,8 @@ npx supabase secrets set \ +Make sure the function has the `deno.json` import map from the [quickstart](/docs/actors/quickstart/supabase) that points `rivetkit` at `@rivetkit/supabase`. It keeps the deploy to the WebAssembly runtime; without it the deploy pulls Rivet's native engine and fails with a `413` (request too large). + ```sh npx supabase functions deploy rivet ```