Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/clean-declaration-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Keep client inspection and health types independent of runtime validators, and include transitive vendored declaration chunks in the package. Framework integrations now declare their optional peer dependencies so package managers can report unsupported versions.
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"@upstash/redis": "^1.38.4",
"@vectorize-io/hindsight-eve": "^0.2.1",
"@veltdev/chat-sdk-adapter": "0.2.4",
"@vercel/connect": "2.2.0",
"@vercel/connect": "catalog:",
"@vercel/otel": "catalog:",
"@vgpu/adapter-node": "0.0.7",
"@webgpu/types": "^0.1.69",
Expand Down
2 changes: 1 addition & 1 deletion apps/frameworks/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"@auth/core": "0.41.2",
"@vercel/connect": "2.2.0",
"@vercel/connect": "catalog:",
"ai": "catalog:",
"eve": "workspace:*",
"next": "catalog:",
Expand Down
19 changes: 19 additions & 0 deletions docs/reference/typescript-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ export default defineTool({
});
```

## Integration dependencies

Install the framework packages for the integration you import. These are optional peers of eve;
using another entry does not require them.

| Entry | Required integration packages |
| ------------------------------------------ | -------------------------------------- |
| `eve/react` | `react` 19 |
| `eve/vue` | `vue` 3.5 or later in v3 |
| `eve/svelte` | `svelte` 5 |
| `eve/next` | `next` 16 and `react` 19 |
| `eve/sveltekit`, `eve/tanstack` | `vite` 8.3.1 or later in v8 |
| `eve/nuxt` | `@nuxt/kit` 4 and `@nuxt/schema` 4 |
| `@vercel/connect/eve` with eve connections | `@vercel/connect` 2.5.2 or later in v2 |

The AI SDK is a required peer and has its own Zod peer requirement. Client inspection types
(`AgentInfoResult`, its entry types, and `HealthResult`) are eve-owned data contracts, not
inferences from eve's runtime validators.

## The define\* helpers

| Helper | Import from | Authored at | Guide |
Expand Down
34 changes: 33 additions & 1 deletion packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -658,15 +658,47 @@
"zod-validation-error": "5.0.0"
},
"peerDependencies": {
"@nuxt/kit": "^4.0.0",
"@nuxt/schema": "^4.0.0",
"@opentelemetry/api": "^1.0.0",
"@vercel/connect": "^2.5.2",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ^2.5.2 floor conflicts with workspace consumers of eve: workspace:* that pin @vercel/connect to 2.2.0: apps/frameworks/next/package.json:16 and apps/docs/package.json:92. Optional peers are still range-checked when they're installed, so these now produce unmet-peer warnings, and they'd fail under strict peer validation. Either bump those apps to 2.5.2 (the catalog version), or set the floor to whatever version first shipped @vercel/connect/manifest, which src/internal/external-resources.ts resolves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed resolved by 863da68: apps/frameworks/next and apps/docs now pull @vercel/connect from catalog:, which is pinned to 2.5.2 in pnpm-workspace.yaml and satisfies eve's ^2.5.2 optional-peer floor. The remaining apps/templates/* consumers use the ^2.4.0 range (not an exact 2.2.0 pin), so they resolve to 2.5.2 and don't trip the peer check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the 2.5.2 floor: the installed 2.2.0 packages do not export /manifest. Both workspace consumers now use the catalog version.

"ai": "catalog:",
"braintrust": "^3.0.0",
"chat": "^4.42.0",
"dd-trace": "^6.13.0",
"just-bash": "^3.1.0",
"microsandbox": "^0.5.0"
"microsandbox": "^0.5.0",
"next": "^16.0.0",
"react": "^19.0.0",
"svelte": "^5.0.0",
"vite": "^8.3.1",
"vue": "^3.5.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@nuxt/schema": {
"optional": true
},
"@vercel/connect": {
"optional": true
},
"next": {
"optional": true
},
"react": {
"optional": true
},
"svelte": {
"optional": true
},
"vite": {
"optional": true
},
"vue": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
Expand Down
134 changes: 115 additions & 19 deletions packages/eve/scripts/vendor-compiled/_shared.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import { createHash } from "node:crypto";
import { realpathSync } from "node:fs";
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { createRequire, isBuiltin } from "node:module";
import { dirname, isAbsolute, join, parse, posix, relative } from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -52,6 +52,7 @@ import { onVendoredDependencyLog } from "../../src/internal/bundler/vendored-dep

const declarationsDir = fileURLToPath(new URL("./declarations/", import.meta.url));
const require = createRequire(import.meta.url);
const ts = createRequire(require.resolve("@microsoft/api-extractor/package.json"))("typescript");

// ───────────────────────────────────────────────────────────────────────────
// Per-package authoring API
Expand Down Expand Up @@ -105,9 +106,9 @@ export async function loadDeclaration(relativePath) {
* `declarationRoot` changes the package-relative source directory when
* declarations live outside `dist`.
*
* `discoverExtraFiles` is consulted for chunk files the upstream `.d.ts`
* references by relative path (e.g. chat's `./jsx-runtime-<hash>.d.ts`).
* Each returned filename is co-copied verbatim into the destination.
* Relative declaration imports are followed recursively, including hashed chunks,
* and receive the same external rewrite rules as the entry declarations.
* `discoverExtraFiles` can additionally copy non-declaration assets verbatim.
*/
export function createDeclarationCopier({
declarationRoot = "dist",
Expand All @@ -125,12 +126,61 @@ export function createDeclarationCopier({
? files
: [{ source: "index.d.ts", output: "index.d.ts" }];

const declarations = await Promise.all(
declarationFiles.map(async (file) => ({
...file,
sourceText: await readFile(join(distDir, file.source), "utf8"),
})),
);
const declarations = [];
const queued = new Map(declarationFiles.map((file) => [file.source, file]));
const extras = typeof discoverExtraFiles === "function" ? discoverExtraFiles(distEntries) : [];
for (const file of extras.filter((file) => /\.d\.[cm]?ts$/u.test(file))) {
if (!queued.has(file)) queued.set(file, { source: file, output: file });
}
for (const file of queued.values()) {
const sourceText = await readFile(join(distDir, file.source), "utf8");
declarations.push({ ...file, sourceText });
for (const specifier of collectRelativeDeclarationReferences(sourceText)) {
const candidates = /\.d\.[cm]?ts$/u.test(specifier)
? [specifier]
: /\.[cm]?js$/u.test(specifier)
? [
specifier
.replace(/\.js$/u, ".d.ts")
.replace(/\.mjs$/u, ".d.mts")
.replace(/\.cjs$/u, ".d.cts"),
]
: [`${specifier}.d.ts`, `${specifier}/index.d.ts`];
let resolved;
for (const candidate of candidates) {
const source = posix.normalize(posix.join(posix.dirname(file.source), candidate));
if (source.startsWith("../") || posix.isAbsolute(source)) {
throw new Error(
`Vendor: declaration ${file.source} escapes ${declarationRoot}: ${specifier}`,
);
}
try {
if ((await stat(join(distDir, source))).isFile()) {
resolved = source;
break;
}
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
if (resolved === undefined) {
throw new Error(
`Vendor: ${packageInfo.packageJson.name}'s declaration ${file.source} imports missing declaration ${specifier}`,
);
}
if (!queued.has(resolved)) {
queued.set(resolved, {
source: resolved,
output: posix.normalize(
posix.join(
posix.dirname(file.output),
posix.relative(posix.dirname(file.source), resolved),
),
),
});
}
}
}
const externals = mergeExternalDeclarationImports(
declarations.map((declaration) => declaration.sourceText),
);
Expand Down Expand Up @@ -183,19 +233,46 @@ export function createDeclarationCopier({
}),
);

if (typeof discoverExtraFiles === "function") {
const extras = discoverExtraFiles(distEntries);
if (extras.length > 0) {
await Promise.all(
extras.map(async (file) => {
const outputPath = join(destinationRoot, file);
await mkdir(dirname(outputPath), { recursive: true });
await copyFile(join(distDir, file), outputPath);
}),
extras
.filter((file) => !/\.d\.[cm]?ts$/u.test(file))
.map(async (file) => {
const outputPath = join(destinationRoot, file);
await mkdir(dirname(outputPath), { recursive: true });
await copyFile(join(distDir, file), outputPath);
}),
);
}
};
}

function collectRelativeDeclarationReferences(source) {
const file = ts.createSourceFile("declarations.d.ts", source, ts.ScriptTarget.Latest, true);
const references = new Set(
file.referencedFiles.map(({ fileName }) =>
fileName.startsWith(".") ? fileName : `./${fileName}`,
),
);
function visit(node) {
let specifier;
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
specifier = node.moduleSpecifier;
else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument))
specifier = node.argument.literal;
else if (
ts.isImportEqualsDeclaration(node) &&
ts.isExternalModuleReference(node.moduleReference)
)
specifier = node.moduleReference.expression;
if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith("."))
references.add(specifier.text);
ts.forEachChild(node, visit);
}
visit(file);
return references;
}

function mergeExternalDeclarationImports(sources) {
const result = new Map();
for (const source of sources) {
Expand Down Expand Up @@ -247,7 +324,7 @@ export function collectExternalDeclarationImports(source) {
!moduleName.startsWith(".") &&
!moduleName.startsWith("/") &&
!moduleName.startsWith("#") &&
!moduleName.startsWith("node:");
!isBuiltin(moduleName);

const addName = (moduleName, name) => {
if (!isExternal(moduleName)) {
Expand Down Expand Up @@ -301,6 +378,21 @@ export function collectExternalDeclarationImports(source) {
addName(match[1]);
}

const file = ts.createSourceFile("declarations.d.ts", source, ts.ScriptTarget.Latest, true);
function visit(node) {
if (
ts.isImportTypeNode(node) &&
ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteral(node.argument.literal)
) {
let qualifier = node.qualifier;
while (qualifier && ts.isQualifiedName(qualifier)) qualifier = qualifier.left;
addName(node.argument.literal.text, qualifier?.text);
}
ts.forEachChild(node, visit);
}
visit(file);

return result;
}

Expand All @@ -316,7 +408,11 @@ export function rewriteImportSource(source, moduleName, replacement) {
.replaceAll(new RegExp(`from '${escaped}'`, "g"), `from '${replacement}'`)
.replaceAll(new RegExp(`from "${escaped}"`, "g"), `from "${replacement}"`)
.replaceAll(new RegExp(`import '${escaped}'`, "g"), `import '${replacement}'`)
.replaceAll(new RegExp(`import "${escaped}"`, "g"), `import "${replacement}"`);
.replaceAll(new RegExp(`import "${escaped}"`, "g"), `import "${replacement}"`)
.replaceAll(
new RegExp(`import\\s*\\(\\s*(["'])${escaped}\\1\\s*\\)`, "g"),
`import("${replacement}")`,
);
}

/**
Expand Down
44 changes: 20 additions & 24 deletions packages/eve/src/client/agent-info-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,27 +471,23 @@ function normalizeRoutePattern(path: string): string {
.join("/");
}

type ReadonlyDeep<T> = T extends readonly (infer Item)[]
? readonly ReadonlyDeep<Item>[]
: T extends object
? { readonly [Key in keyof T]: ReadonlyDeep<T[Key]> }
: T;

export type AgentInfoResult = ReadonlyDeep<z.output<typeof AgentInfoResultSchema>>;
export type AgentInfoSource = ReadonlyDeep<z.output<typeof source>>;
export type AgentInfoEntry = ReadonlyDeep<z.output<typeof entry>>;
export type AgentInfoToolEntry = ReadonlyDeep<z.output<typeof tool>>;
export type AgentInfoDynamicResolverEntry = ReadonlyDeep<z.output<typeof dynamicResolver>>;
export type AgentInfoTools = AgentInfoResult["tools"];
export type AgentInfoSkillEntry = ReadonlyDeep<z.output<typeof skill>>;
export type AgentInfoInstructionsEntry = ReadonlyDeep<z.output<typeof instructions>>;
export type AgentInfoInstructions = AgentInfoResult["instructions"];
export type AgentInfoScheduleEntry = ReadonlyDeep<z.output<typeof schedule>>;
export type AgentInfoSubagentEntry = ReadonlyDeep<z.output<typeof subagent>>;
export type AgentInfoRemoteAgentEntry = ReadonlyDeep<z.output<typeof remoteAgent>>;
export type AgentInfoChannelEntry = ReadonlyDeep<z.output<typeof channelRoute>>;
export type AgentInfoChannels = AgentInfoResult["channels"];
export type AgentInfoConnectionEntry = ReadonlyDeep<z.output<typeof connection>>;
export type AgentInfoHookEntry = ReadonlyDeep<z.output<typeof hook>>;
export type AgentInfoMemoryEntry = ReadonlyDeep<z.output<typeof memory>>;
export type AgentInfoSandboxEntry = ReadonlyDeep<z.output<typeof sandbox>>;
export type {
AgentInfoResult,
AgentInfoSource,
AgentInfoEntry,
AgentInfoToolEntry,
AgentInfoDynamicResolverEntry,
AgentInfoTools,
AgentInfoSkillEntry,
AgentInfoInstructionsEntry,
AgentInfoInstructions,
AgentInfoScheduleEntry,
AgentInfoSubagentEntry,
AgentInfoRemoteAgentEntry,
AgentInfoChannelEntry,
AgentInfoChannels,
AgentInfoConnectionEntry,
AgentInfoHookEntry,
AgentInfoMemoryEntry,
AgentInfoSandboxEntry,
} from "./agent-info-types.js";
Loading
Loading