diff --git a/package.json b/package.json
index bb080a8..7a40656 100644
--- a/package.json
+++ b/package.json
@@ -147,7 +147,7 @@
     "test": "vitest --silent",
     "test-not-silent": "vitest",
     "new-test": "vitest",
-    "test-esm": "node ./scripts/test-esm.mjs && ./scripts/checkdeps.mjs",
+    "test-esm": "node ./scripts/test-esm.mjs && ./scripts/checkdeps.mjs && ./scripts/checkimports.mjs",
     "pack-internal": "echo TODO maybe set an environment variable"
   },
   "keywords": [
@@ -164,8 +164,7 @@
   "dependencies": {
     "esbuild": "0.23.0",
     "jwt-decode": "^4.0.0",
-    "prettier": "3.2.5",
-    "globals": "~15.9.0"
+    "prettier": "3.2.5"
   },
   "peerDependencies": {
     "@auth0/auth0-react": "^2.0.1",
@@ -234,6 +233,7 @@
     "eslint-plugin-require-extensions": "~0.1.3",
     "fetch-retry": "~5.0.6",
     "find-up": "^6.3.0",
+    "globals": "~15.9.0",
     "happy-dom": "~14.12.3",
     "inquirer": "^9.1.4",
     "inquirer-search-list": "~1.2.6",
@@ -247,6 +247,7 @@
     "react-dom": "^18.0.0",
     "semver": "^7.6.0",
     "shx": "~0.3.4",
+    "skott": "~0.35.3",
     "strip-ansi": "^7.0.1",
     "tsx": "~4.15.6",
     "typedoc": "^0.24.6",
diff --git a/scripts/checkimports.mjs b/scripts/checkimports.mjs
new file mode 100755
index 0000000..e36f5c4
--- /dev/null
+++ b/scripts/checkimports.mjs
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+import { fileURLToPath } from "url";
+import { dirname } from "path";
+import skott from "skott";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const __root = dirname(__dirname);
+
+async function entrypointHasCycles(entrypoint) {
+  // Note that skott can do a lot of other things too!
+  const { useGraph } = await skott({
+    entrypoint: `./dist/esm/${entrypoint}/index.js`,
+    incremental: false,
+    cwd: __root,
+    includeBaseDir: true,
+    verbose: false,
+  });
+  const { findCircularDependencies } = useGraph();
+
+  const circular = findCircularDependencies();
+  if (circular.length) {
+    console.log("Found import cycles by traversing", entrypoint);
+    console.log(circular);
+    return false;
+  }
+  return true;
+}
+
+let allOk = true;
+// These haven't been fixed yet so we don't fail if they have cycles.
+for (const entrypoint of [
+  "bundler",
+  "nextjs",
+  "react",
+  "react-auth0",
+  "react-clerk",
+  "values",
+  // don't care about cycles in CLI
+]) {
+  const ok = await entrypointHasCycles(entrypoint);
+  allOk &&= ok;
+}
+
+if (!(await entrypointHasCycles("server"))) {
+  process.exit(1);
+} else {
+  console.log("No import cycles found in server.");
+  process.exit(0);
+}
diff --git a/src/server/api.ts b/src/server/api.ts
index 9b36e7f..91f7fba 100644
--- a/src/server/api.ts
+++ b/src/server/api.ts
@@ -9,6 +9,7 @@ import {
 import { Expand, UnionToIntersection } from "../type_utils.js";
 import { PaginationOptions, PaginationResult } from "./pagination.js";
 import { getFunctionAddress } from "./impl/actions_impl.js";
+import { functionName } from "./functionName.js";
 
 /**
  * The type of a Convex function.
@@ -62,11 +63,6 @@ export type FunctionReference<
   _componentPath: ComponentPath;
 };
 
-/**
- * A symbol for accessing the name of a {@link FunctionReference} at runtime.
- */
-export const functionName = Symbol.for("functionName");
-
 /**
  * Get the name of a function from a {@link FunctionReference}.
  *
diff --git a/src/server/components/index.ts b/src/server/components/index.ts
index 39896b8..0dc9377 100644
--- a/src/server/components/index.ts
+++ b/src/server/components/index.ts
@@ -12,16 +12,7 @@ import {
   ComponentDefinitionAnalysis,
   ComponentDefinitionType,
 } from "./definition.js";
-
-export const toReferencePath = Symbol.for("toReferencePath");
-
-export function extractReferencePath(reference: any): string | null {
-  return reference[toReferencePath] ?? null;
-}
-
-export function isFunctionHandle(s: string): boolean {
-  return s.startsWith("function://");
-}
+import { toReferencePath } from "./paths.js";
 
 /**
  * @internal
diff --git a/src/server/components/paths.ts b/src/server/components/paths.ts
new file mode 100644
index 0000000..4f76e98
--- /dev/null
+++ b/src/server/components/paths.ts
@@ -0,0 +1,9 @@
+export const toReferencePath = Symbol.for("toReferencePath");
+
+export function extractReferencePath(reference: any): string | null {
+  return reference[toReferencePath] ?? null;
+}
+
+export function isFunctionHandle(s: string): boolean {
+  return s.startsWith("function://");
+}
diff --git a/src/server/functionName.ts b/src/server/functionName.ts
new file mode 100644
index 0000000..a0edd65
--- /dev/null
+++ b/src/server/functionName.ts
@@ -0,0 +1,4 @@
+/**
+ * A symbol for accessing the name of a {@link FunctionReference} at runtime.
+ */
+export const functionName = Symbol.for("functionName");
diff --git a/src/server/functions.ts b/src/server/functions.ts
new file mode 100644
index 0000000..a0edd65
--- /dev/null
+++ b/src/server/functions.ts
@@ -0,0 +1,4 @@
+/**
+ * A symbol for accessing the name of a {@link FunctionReference} at runtime.
+ */
+export const functionName = Symbol.for("functionName");
diff --git a/src/server/impl/actions_impl.ts b/src/server/impl/actions_impl.ts
index 367c9c4..d5aef3d 100644
--- a/src/server/impl/actions_impl.ts
+++ b/src/server/impl/actions_impl.ts
@@ -2,8 +2,9 @@ import { convexToJson, jsonToConvex, Value } from "../../values/index.js";
 import { version } from "../../index.js";
 import { performAsyncSyscall } from "./syscall.js";
 import { parseArgs } from "../../common/index.js";
-import { functionName, FunctionReference } from "../../server/api.js";
-import { extractReferencePath, isFunctionHandle } from "../components/index.js";
+import { FunctionReference } from "../../server/api.js";
+import { extractReferencePath, isFunctionHandle } from "../components/paths.js";
+import { functionName } from "../functionName.js";
 
 function syscallArgs(
   requestId: string,