From f0d13f9897ee0dcd9083d855ef4e0ea0249ac850 Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Fri, 21 Nov 2025 20:18:35 +0100
Subject: [PATCH] feat(devtools): add devtools
---
pnpm-lock.yaml | 41 ++++++++++
.../packages/devtools/README.md | 28 +++++++
.../packages/devtools/package.json | 40 +++++++++
.../packages/devtools/src/globals.d.ts | 9 +++
.../packages/devtools/src/icon.svg | 1 +
.../packages/devtools/src/mod.ts | 68 ++++++++++++++++
.../packages/devtools/src/styles.css | 81 +++++++++++++++++++
.../packages/devtools/tsconfig.json | 24 ++++++
.../packages/devtools/tsup.config.ts | 10 +++
.../packages/devtools/turbo.json | 15 ++++
.../packages/rivetkit/src/client/config.ts | 9 +++
.../packages/rivetkit/src/client/mod.ts | 6 ++
.../packages/rivetkit/src/devtools/mod.ts | 31 +++++++
.../packages/rivetkit/src/registry/mod.ts | 1 +
.../packages/rivetkit/tsup.config.ts | 8 ++
.../packages/rivetkit/turbo.json | 2 +-
16 files changed, 373 insertions(+), 1 deletion(-)
create mode 100644 rivetkit-typescript/packages/devtools/README.md
create mode 100644 rivetkit-typescript/packages/devtools/package.json
create mode 100644 rivetkit-typescript/packages/devtools/src/globals.d.ts
create mode 100644 rivetkit-typescript/packages/devtools/src/icon.svg
create mode 100644 rivetkit-typescript/packages/devtools/src/mod.ts
create mode 100644 rivetkit-typescript/packages/devtools/src/styles.css
create mode 100644 rivetkit-typescript/packages/devtools/tsconfig.json
create mode 100644 rivetkit-typescript/packages/devtools/tsup.config.ts
create mode 100644 rivetkit-typescript/packages/devtools/turbo.json
create mode 100644 rivetkit-typescript/packages/rivetkit/src/devtools/mod.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 099dc54305..08b39fc9bb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2184,6 +2184,18 @@ importers:
specifier: ^3.1.1
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)
+ rivetkit-typescript/packages/devtools:
+ devDependencies:
+ rivetkit:
+ specifier: workspace:*
+ version: link:../rivetkit
+ tsup:
+ specifier: ^8.4.0
+ version: 8.5.0(@microsoft/api-extractor@7.53.2(@types/node@24.10.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
+ typescript:
+ specifier: ^5.5.2
+ version: 5.9.3
+
rivetkit-typescript/packages/framework-base:
dependencies:
'@tanstack/store':
@@ -27203,6 +27215,35 @@ snapshots:
- tsx
- yaml
+ tsup@8.5.0(@microsoft/api-extractor@7.53.2(@types/node@24.10.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1):
+ dependencies:
+ bundle-require: 5.1.0(esbuild@0.25.9)
+ cac: 6.7.14
+ chokidar: 4.0.3
+ consola: 3.4.2
+ debug: 4.4.1
+ esbuild: 0.25.9
+ fix-dts-default-cjs-exports: 1.0.1
+ joycon: 3.1.1
+ picocolors: 1.1.1
+ postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1)
+ resolve-from: 5.0.0
+ rollup: 4.50.1
+ source-map: 0.8.0-beta.0
+ sucrase: 3.35.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ optionalDependencies:
+ '@microsoft/api-extractor': 7.53.2(@types/node@24.10.1)
+ postcss: 8.5.6
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - jiti
+ - supports-color
+ - tsx
+ - yaml
+
tsup@8.5.0(@microsoft/api-extractor@7.53.2(@types/node@24.7.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.8.1):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.9)
diff --git a/rivetkit-typescript/packages/devtools/README.md b/rivetkit-typescript/packages/devtools/README.md
new file mode 100644
index 0000000000..da73e2d45c
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/README.md
@@ -0,0 +1,28 @@
+# RivetKit DevTools
+
+
+## Contributing
+
+To contribute to the RivetKit DevTools package, please follow these steps:
+
+1. Set up assets server for the `dist` folder:
+ ```bash
+ pnpm dlx serve dist
+ ```
+
+2. Set your `CUSTOM_RIVETKIT_DEVTOOLS_URL` environment variable to point to the assets server (default is `http://localhost:3000`):
+ ```bash
+ export CUSTOM_RIVETKIT_DEVTOOLS_URL=http://localhost:5000
+ ```
+
+ This will ensure that the RivetKit will use local devtool assets instead of fetching them from the CDN.
+
+3. In another terminal, run the development build:
+ ```bash
+ pnpm dev
+ ```
+
+ or run the production build:
+ ```bash
+ pnpm build
+ ```
diff --git a/rivetkit-typescript/packages/devtools/package.json b/rivetkit-typescript/packages/devtools/package.json
new file mode 100644
index 0000000000..11fbe69829
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@rivetkit/devtools",
+ "private": true,
+ "version": "2.0.24-rc.1",
+ "description": "RivetKit DevTools - A set of development tools for RivetKit",
+ "license": "Apache-2.0",
+ "keywords": [
+ "rivetkit"
+ ],
+ "sideEffects": [
+ "./dist/chunk-*.js",
+ "./dist/chunk-*.cjs"
+ ],
+ "files": [
+ "dist",
+ "package.json"
+ ],
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/mod.d.mts",
+ "default": "./dist/mod.mjs"
+ },
+ "require": {
+ "types": "./dist/mod.d.ts",
+ "default": "./dist/mod.js"
+ }
+ }
+ },
+ "scripts": {
+ "build": "tsup src/mod.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "tsup": "^8.4.0",
+ "typescript": "^5.5.2",
+ "rivetkit": "workspace:*"
+ },
+ "stableVersion": "0.8.0"
+}
diff --git a/rivetkit-typescript/packages/devtools/src/globals.d.ts b/rivetkit-typescript/packages/devtools/src/globals.d.ts
new file mode 100644
index 0000000000..84210ac866
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/src/globals.d.ts
@@ -0,0 +1,9 @@
+declare module "*.css" {
+ const content: string;
+ export default content;
+}
+
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
diff --git a/rivetkit-typescript/packages/devtools/src/icon.svg b/rivetkit-typescript/packages/devtools/src/icon.svg
new file mode 100644
index 0000000000..287c425791
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/src/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/rivetkit-typescript/packages/devtools/src/mod.ts b/rivetkit-typescript/packages/devtools/src/mod.ts
new file mode 100644
index 0000000000..381c6c30ab
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/src/mod.ts
@@ -0,0 +1,68 @@
+import svg from "./icon.svg";
+import styles from "./styles.css";
+
+declare global {
+ interface Window {
+ _rivetkit_devtools_configs?: Array<
+ Parameters[0]
+ >;
+ }
+}
+
+const root = document.createElement("div");
+
+root.id = "rivetkit-devtools";
+const shadow = root.attachShadow({ mode: "open" });
+
+const div = document.createElement("div");
+
+const img = document.createElement("img");
+img.src = svg;
+div.appendChild(img);
+
+const btn = document.createElement("button");
+btn.appendChild(div);
+
+const tooltip = document.createElement("div");
+tooltip.className = "tooltip";
+tooltip.textContent = "Open Inspector";
+
+const style = document.createElement("style");
+style.textContent = styles;
+shadow.appendChild(style);
+shadow.appendChild(btn);
+shadow.appendChild(tooltip);
+
+btn.addEventListener("mouseenter", () => {
+ tooltip.classList.add("visible");
+});
+
+btn.addEventListener("mouseleave", () => {
+ tooltip.classList.remove("visible");
+});
+
+btn.addEventListener("click", () => {
+ const config = window._rivetkit_devtools_configs?.[0];
+ if (!config || typeof config !== "object") {
+ console.error("RivetKit Devtools: No client config found");
+ return;
+ }
+ const url = new URL("https://inspect.rivet.dev/");
+ if (!config.endpoint) {
+ console.error("RivetKit Devtools: No endpoint found in client config");
+ return;
+ }
+ url.searchParams.set("u", config.endpoint);
+ if (config.token) {
+ url.searchParams.set("t", config.token);
+ }
+ if (config.namespace) {
+ url.searchParams.set("ns", config.namespace);
+ }
+ if (config.runnerName) {
+ url.searchParams.set("r", config.runnerName);
+ }
+ window.open(url.toString(), "_blank");
+});
+
+document.body.appendChild(root);
diff --git a/rivetkit-typescript/packages/devtools/src/styles.css b/rivetkit-typescript/packages/devtools/src/styles.css
new file mode 100644
index 0000000000..a4a703a8e2
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/src/styles.css
@@ -0,0 +1,81 @@
+:host {
+ all: initial;
+ position: fixed;
+ bottom: 12px;
+ right: 25px;
+ bottom: 25px;
+ z-index: 2147483647;
+ width: 48px;
+ height: 48px;
+}
+
+:host * {
+ box-sizing: border-box;
+}
+
+button {
+ all: unset;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ border-radius: 50%;
+ border: 1px solid #171717;
+ background-color: #09090b;
+ transition: background-color .3s ease;
+}
+
+button:hover {
+ background-color: #0e0e11;
+}
+
+button div {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ border: 1px solid #292525;
+}
+
+img {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+}
+
+.tooltip {
+ position: absolute;
+ right: 60px;
+ bottom: 50%;
+ transform: translateY(50%);
+ background-color: #09090b;
+ color: #fafafa;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .3s ease;
+ border: 1px solid #27272a;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
+}
+
+.tooltip::after {
+ content: "";
+ position: absolute;
+ right: -5px;
+ top: 50%;
+ transform: translateY(-50%) rotate(45deg);
+ width: 8px;
+ height: 8px;
+ background-color: #09090b;
+ border-right: 1px solid #27272a;
+ border-top: 1px solid #27272a;
+}
+
+.tooltip.visible {
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/rivetkit-typescript/packages/devtools/tsconfig.json b/rivetkit-typescript/packages/devtools/tsconfig.json
new file mode 100644
index 0000000000..e1804bad2b
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "lib"]
+}
diff --git a/rivetkit-typescript/packages/devtools/tsup.config.ts b/rivetkit-typescript/packages/devtools/tsup.config.ts
new file mode 100644
index 0000000000..b469e5af65
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/tsup.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "tsup";
+import defaultConfig from "../../../tsup.base.ts";
+
+export default defineConfig({
+ ...defaultConfig,
+ loader: {
+ ".svg": "dataurl",
+ ".css": "text",
+ },
+});
diff --git a/rivetkit-typescript/packages/devtools/turbo.json b/rivetkit-typescript/packages/devtools/turbo.json
new file mode 100644
index 0000000000..ba682d5df2
--- /dev/null
+++ b/rivetkit-typescript/packages/devtools/turbo.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build": {
+ "inputs": [
+ "src/**",
+ "tsconfig.json",
+ "tsup.config.ts",
+ "package.json"
+ ],
+ "outputs": ["dist/**"]
+ }
+ }
+}
diff --git a/rivetkit-typescript/packages/rivetkit/src/client/config.ts b/rivetkit-typescript/packages/rivetkit/src/client/config.ts
index 97f9f8d269..149db9efde 100644
--- a/rivetkit-typescript/packages/rivetkit/src/client/config.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/client/config.ts
@@ -39,6 +39,15 @@ export const ClientConfigSchema = z.object({
/** Whether to automatically perform health checks when the client is created. */
disableMetadataLookup: z.boolean().optional().default(false),
+
+ /** Whether to enable RivetKit Devtools integration. */
+ devtools: z
+ .boolean()
+ .default(
+ () =>
+ typeof globalThis.window !== "undefined" &&
+ window?.location?.hostname === "localhost",
+ ),
});
export type ClientConfig = z.infer;
diff --git a/rivetkit-typescript/packages/rivetkit/src/client/mod.ts b/rivetkit-typescript/packages/rivetkit/src/client/mod.ts
index d4b8d27766..e44c25ca0f 100644
--- a/rivetkit-typescript/packages/rivetkit/src/client/mod.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/client/mod.ts
@@ -1,3 +1,4 @@
+import { injectDevtools } from "@/devtools/mod";
import type { Registry } from "@/registry/mod";
import { RemoteManagerDriver } from "@/remote-manager-driver/mod";
import {
@@ -55,5 +56,10 @@ export function createClient>(
// Create client
const driver = new RemoteManagerDriver(config);
+
+ if (config.devtools) {
+ injectDevtools(config);
+ }
+
return createClientWithDriver(driver, config);
}
diff --git a/rivetkit-typescript/packages/rivetkit/src/devtools/mod.ts b/rivetkit-typescript/packages/rivetkit/src/devtools/mod.ts
new file mode 100644
index 0000000000..13244ac12d
--- /dev/null
+++ b/rivetkit-typescript/packages/rivetkit/src/devtools/mod.ts
@@ -0,0 +1,31 @@
+///
+
+declare global {
+ interface Window {
+ _rivetkit_devtools_configs?: ClientConfigInput[];
+ }
+ // injected via tsup config
+ var CUSTOM_RIVETKIT_DEVTOOLS_URL: string | undefined;
+}
+
+import type { ClientConfigInput } from "@/client/client";
+import { VERSION } from "@/utils";
+
+const DEVTOOLS_URL = (version = VERSION) =>
+ `https://releases.rivet.gg/devtools/${version}/rivetkit-devtools.js`;
+
+const scriptId = "rivetkit-devtools-script";
+
+export function injectDevtools(config: ClientConfigInput) {
+ if (!document.getElementById(scriptId)) {
+ const script = document.createElement("script");
+ script.id = scriptId;
+ script.src = globalThis.CUSTOM_RIVETKIT_DEVTOOLS_URL || DEVTOOLS_URL();
+ script.async = true;
+ document.head.appendChild(script);
+ }
+
+ window._rivetkit_devtools_configs = window._rivetkit_devtools_configs || [];
+ window._rivetkit_devtools_configs.push(config);
+ return;
+}
diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts b/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts
index 24622a5518..3725caab8a 100644
--- a/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts
+++ b/rivetkit-typescript/packages/rivetkit/src/registry/mod.ts
@@ -291,6 +291,7 @@ async function configureServerlessRunner(config: RunnerConfig): Promise {
headers: config.headers,
getUpgradeWebSocket: config.getUpgradeWebSocket,
disableMetadataLookup: true, // We don't need health check for this operation
+ devtools: false,
};
// Fetch all datacenters
diff --git a/rivetkit-typescript/packages/rivetkit/tsup.config.ts b/rivetkit-typescript/packages/rivetkit/tsup.config.ts
index d8652c0151..e8e9cec121 100644
--- a/rivetkit-typescript/packages/rivetkit/tsup.config.ts
+++ b/rivetkit-typescript/packages/rivetkit/tsup.config.ts
@@ -1,7 +1,15 @@
+///
+
import { defineConfig } from "tsup";
import defaultConfig from "../../../tsup.base.ts";
export default defineConfig({
...defaultConfig,
outDir: "dist/tsup/",
+ define: {
+ "globalThis.CUSTOM_RIVETKIT_DEVTOOLS_URL": process.env
+ .CUSTOM_RIVETKIT_DEVTOOLS_URL
+ ? `"${process.env.CUSTOM_RIVETKIT_DEVTOOLS_URL}"`
+ : "false",
+ },
});
diff --git a/rivetkit-typescript/packages/rivetkit/turbo.json b/rivetkit-typescript/packages/rivetkit/turbo.json
index 8b13573eb9..8f63714485 100644
--- a/rivetkit-typescript/packages/rivetkit/turbo.json
+++ b/rivetkit-typescript/packages/rivetkit/turbo.json
@@ -35,7 +35,7 @@
"package.json"
],
"outputs": ["dist/**"],
- "env": ["FAST_BUILD"]
+ "env": ["FAST_BUILD", "CUSTOM_RIVETKIT_DEVTOOLS_URL"]
},
"test": {
"dependsOn": ["^test", "build"],