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
14 changes: 14 additions & 0 deletions .changeset/fix-dangling-dts-barrel-imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@tanstack/ai': patch
'@tanstack/ai-anthropic': patch
'@tanstack/ai-bedrock': patch
'@tanstack/ai-fal': patch
'@tanstack/ai-gemini': patch
'@tanstack/ai-grok': patch
'@tanstack/ai-groq': patch
'@tanstack/ai-mistral': patch
'@tanstack/ai-ollama': patch
'@tanstack/ai-openrouter': patch
---

fix: resolve directory-barrel imports in published `.d.ts` files. Bare imports of `utils`/`tools`/`middleware` barrels were emitted as `../utils.js` (etc.), which do not resolve under bundler/node16/nodenext (no `/index` fallback for explicit `.js`). With consumer `skipLibCheck: true` those symbols silently became `any`. Imports now target concrete modules (e.g. `utils/client`, `middleware/types`) or explicit `/index` paths so public types resolve correctly.
7 changes: 7 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
"test:sherif": {
"cache": true,
"inputs": ["{workspaceRoot}/**/package.json"]
},
"test:dts": {
"cache": true,
"inputs": [
"{workspaceRoot}/scripts/scan-dangling-dts.mjs",
"{workspaceRoot}/packages/*/dist/**/*.d.ts"
]
}
}
}
2 changes: 1 addition & 1 deletion packages/ai-anthropic/src/tools/tool-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { convertMemoryToolToAdapterFormat } from './memory-tool'
import { convertTextEditorToolToAdapterFormat } from './text-editor-tool'
import { convertWebFetchToolToAdapterFormat } from './web-fetch-tool'
import { convertWebSearchToolToAdapterFormat } from './web-search-tool'
import type { AnthropicTool } from '.'
import type { AnthropicTool } from './index'
import type { Tool } from '@tanstack/ai'

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-bedrock/src/model-meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated'
import { GENERATED_BEDROCK_MODELS } from './model-catalog.generated.js'
import type { BedrockTextProviderOptions } from './text/text-provider-options'
import type { BedrockConverseProviderOptions } from './converse/provider-options'

Expand Down
40 changes: 20 additions & 20 deletions packages/ai-ollama/src/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { COMMAND_R_7b_MODELS } from './meta/model-meta-command-r7b'
import { DEEPSEEK_CODER_V2_MODELS } from './meta/model-meta-deepseek-coder-v2'
import { DEEPSEEK_OCR_MODELS } from './meta/model-meta-deepseek-ocr'
import { DEEPSEEK_R1_MODELS } from './meta/model-meta-deepseek-r1'
import { DEEPSEEK_V3_1_MODELS } from './meta/model-meta-deepseek-v3.1'
import { DEEPSEEK_V3_1_MODELS } from './meta/model-meta-deepseek-v3.1.js'
import { DEVSTRAL_MODELS } from './meta/model-meta-devstral'
import { DOLPHIN3_MODELS } from './meta/model-meta-dolphin3'
import { EXAONE3_5MODELS } from './meta/model-meta-exaone3.5'
import { EXAONE3_5MODELS } from './meta/model-meta-exaone3.5.js'
import { FALCON2_MODELS } from './meta/model-meta-falcon2'
import { FALCON3_MODELS } from './meta/model-meta-falcon3'
import { FIREFUNCTION_V2_MODELS } from './meta/model-meta-firefunction-v2'
Expand All @@ -23,17 +23,17 @@ import { GPT_OSS_MODELS } from './meta/model-meta-gpt-oss'
import { GRANITE3_DENSE_MODELS } from './meta/model-meta-granite3-dense'
import { GRANITE3_GUARDIAN_MODELS } from './meta/model-meta-granite3-guardian'
import { GRANITE3_MOE_MODELS } from './meta/model-meta-granite3-moe'
import { GRANITE3_1_DENSE_MODELS } from './meta/model-meta-granite3.1-dense'
import { GRANITE3_1_MOE_MODELS } from './meta/model-meta-granite3.1-moe'
import { GRANITE3_1_DENSE_MODELS } from './meta/model-meta-granite3.1-dense.js'
import { GRANITE3_1_MOE_MODELS } from './meta/model-meta-granite3.1-moe.js'
import { LLAMA_GUARD3_MODELS } from './meta/model-meta-llama-guard3'
import { LLAMA2_MODELS } from './meta/model-meta-llama2'
import { LLAMA3_MODELS } from './meta/model-meta-llama3'
import { LLAMA3_CHATQA_MODELS } from './meta/model-meta-llama3-chatqa'
import { LLAMA3_GRADIENT_MODELS } from './meta/model-meta-llama3-gradient'
import { LLAMA3_1_MODELS } from './meta/model-meta-llama3.1'
import { LLAMA3_2_MODELS } from './meta/model-meta-llama3.2'
import { LLAMA3_2_VISION_MODELS } from './meta/model-meta-llama3.2-vision'
import { LLAMA3_3_MODELS } from './meta/model-meta-llama3.3'
import { LLAMA3_1_MODELS } from './meta/model-meta-llama3.1.js'
import { LLAMA3_2_MODELS } from './meta/model-meta-llama3.2.js'
import { LLAMA3_2_VISION_MODELS } from './meta/model-meta-llama3.2-vision.js'
import { LLAMA3_3_MODELS } from './meta/model-meta-llama3.3.js'
import { LLAMA4_MODELS } from './meta/model-meta-llama4'
import { LLAVA_MODELS } from './meta/model-meta-llava'
import { LLAVA_LLAMA3_MODELS } from './meta/model-meta-llava-llama3'
Expand All @@ -54,8 +54,8 @@ import { PHI3_MODELS } from './meta/model-meta-phi3'
import { PHI4_MODELS } from './meta/model-meta-phi4'
import { QWEN_MODELS } from './meta/model-meta-qwen'
import { QWEN2_MODELS } from './meta/model-meta-qwen2'
import { QWEN2_5_MODELS } from './meta/model-meta-qwen2.5'
import { QWEN2_5_CODER_MODELS } from './meta/model-meta-qwen2.5-coder'
import { QWEN2_5_MODELS } from './meta/model-meta-qwen2.5.js'
import { QWEN2_5_CODER_MODELS } from './meta/model-meta-qwen2.5-coder.js'
import { QWEN3_MODELS } from './meta/model-meta-qwen3'
import { QWQ_MODELS } from './meta/model-meta-qwq'
import { SAILOR2_MODELS } from './meta/model-meta-sailor2'
Expand Down Expand Up @@ -109,7 +109,7 @@ import type {
import type {
Deepseekv3_1ChatModelProviderOptionsByName,
Deepseekv3_1ModelInputModalitiesByName,
} from './meta/model-meta-deepseek-v3.1'
} from './meta/model-meta-deepseek-v3.1.js'
import type {
DevstralChatModelProviderOptionsByName,
DevstralModelInputModalitiesByName,
Expand All @@ -121,7 +121,7 @@ import type {
import type {
Exaone3_5ChatModelProviderOptionsByName,
Exaone3_5ModelInputModalitiesByName,
} from './meta/model-meta-exaone3.5'
} from './meta/model-meta-exaone3.5.js'
import type {
Falcon2ChatModelProviderOptionsByName,
Falcon2ModelInputModalitiesByName,
Expand Down Expand Up @@ -165,11 +165,11 @@ import type {
import type {
Granite3_1DenseChatModelProviderOptionsByName,
Granite3_1DenseModelInputModalitiesByName,
} from './meta/model-meta-granite3.1-dense'
} from './meta/model-meta-granite3.1-dense.js'
import type {
Granite3_1MoeChatModelProviderOptionsByName,
Granite3_1MoeModelInputModalitiesByName,
} from './meta/model-meta-granite3.1-moe'
} from './meta/model-meta-granite3.1-moe.js'
import type {
LlamaGuard3ChatModelProviderOptionsByName,
LlamaGuard3ModelInputModalitiesByName,
Expand All @@ -193,19 +193,19 @@ import type {
import type {
Llama3_1ChatModelProviderOptionsByName,
Llama3_1ModelInputModalitiesByName,
} from './meta/model-meta-llama3.1'
} from './meta/model-meta-llama3.1.js'
import type {
Llama3_2ChatModelProviderOptionsByName,
Llama3_2ModelInputModalitiesByName,
} from './meta/model-meta-llama3.2'
} from './meta/model-meta-llama3.2.js'
import type {
Llama3_2VisionChatModelProviderOptionsByName,
Llama3_2VisionModelInputModalitiesByName,
} from './meta/model-meta-llama3.2-vision'
} from './meta/model-meta-llama3.2-vision.js'
import type {
Llama3_3ChatModelProviderOptionsByName,
Llama3_3ModelInputModalitiesByName,
} from './meta/model-meta-llama3.3'
} from './meta/model-meta-llama3.3.js'
import type {
Llama4ChatModelProviderOptionsByName,
Llama4ModelInputModalitiesByName,
Expand Down Expand Up @@ -289,11 +289,11 @@ import type {
import type {
Qwen2_5ChatModelProviderOptionsByName,
Qwen2_5ModelInputModalitiesByName,
} from './meta/model-meta-qwen2.5'
} from './meta/model-meta-qwen2.5.js'
import type {
Qwen2_5CoderChatModelProviderOptionsByName,
Qwen2_5CoderModelInputModalitiesByName,
} from './meta/model-meta-qwen2.5-coder'
} from './meta/model-meta-qwen2.5-coder.js'
import type {
Qwen3ChatModelProviderOptionsByName,
Qwen3ModelInputModalitiesByName,
Expand Down
137 changes: 102 additions & 35 deletions scripts/scan-dangling-dts.mjs
Original file line number Diff line number Diff line change
@@ -1,54 +1,121 @@
// scan-dangling-dts.mjs — run from repo root after `pnpm build:all`
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
import { dirname, resolve, join } from 'node:path'
#!/usr/bin/env node
/**
* Scan built package declarations for dangling relative imports.
*
* Under bundler/node16/nodenext resolution, an explicit `.js` specifier is
* remapped to a sibling `.d.ts` and does **not** fall back to `/index`.
* Directory-barrel imports emitted as `../utils.js` therefore fail to resolve
* (the real file is `utils/index.d.ts`). Consumers usually set
* `skipLibCheck: true`, so the unresolved import silently becomes `any`
* instead of erroring — degrading public types without a signal.
*
* This check runs on the producer side over built package dist declarations so
* regressions fail CI regardless of consumer tsconfig.
*
* Run after a packages build:
* pnpm build:all && pnpm test:dts
*/
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const ROOT = process.cwd()
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const PACKAGES_DIR = join(ROOT, 'packages')

function walk(dir, out = []) {
for (const e of readdirSync(dir)) {
const p = join(dir, e)
const st = statSync(p)
/** @param {string} dir @param {string[]} out */
function walkDts(dir, out = []) {
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
const st = statSync(path)
if (st.isDirectory()) {
if (e !== 'node_modules') walk(p, out)
} else if (e.endsWith('.d.ts')) {
out.push(p)
if (entry !== 'node_modules') walkDts(path, out)
} else if (entry.endsWith('.d.ts') && !entry.endsWith('.d.ts.map')) {
out.push(path)
}
}
return out
}

const RE = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+)['"]/g
/**
* Whether a relative declaration import resolves to a real file.
* Explicit `.js`/`.mjs`/`.cjs` extensions do **not** fall back to `/index`
* (matching TypeScript bundler/node16/nodenext behavior).
*
* @param {string} fromFile
* @param {string} specifier
*/
function resolves(fromFile, specifier) {
const abs = resolve(dirname(fromFile), specifier)

if (/\.(js|mjs|cjs)$/.test(specifier)) {
const noext = abs.replace(/\.(js|mjs|cjs)$/, '')
return ['.d.ts', '.d.mts', '.d.cts', '.ts', '.tsx'].some((ext) =>
existsSync(noext + ext),
)
}

return (
['.d.ts', '.ts', '.tsx'].some((ext) => existsSync(abs + ext)) ||
['/index.d.ts', '/index.ts'].some((suffix) => existsSync(abs + suffix))
)
}

const IMPORT_RE = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+)['"]/g

const packageNames = existsSync(PACKAGES_DIR)
? readdirSync(PACKAGES_DIR).filter((name) => {
try {
return statSync(join(PACKAGES_DIR, name)).isDirectory()
} catch {
return false
}
})
: []

const dists = packageNames
.map((name) => join(PACKAGES_DIR, name, 'dist'))
.filter((dir) => existsSync(dir))

if (dists.length === 0) {
console.error(
'scan-dangling-dts: no packages/*/dist directories found. Build packages first (e.g. pnpm build:all).',
)
process.exit(1)
}

/** @type {string[]} */
const findings = []
const dists = readdirSync(join(ROOT, 'packages'))
.map((p) => join(ROOT, 'packages', p, 'dist'))
.filter((d) => existsSync(d))
let filesScanned = 0

for (const dist of dists) {
for (const file of walk(dist)) {
for (const file of walkDts(dist)) {
filesScanned += 1
const src = readFileSync(file, 'utf8')
let m
while ((m = RE.exec(src))) {
const spec = m[1]
const abs = resolve(dirname(file), spec)
let ok
if (/\.(js|mjs|cjs)$/.test(spec)) {
const noext = abs.replace(/\.(js|mjs|cjs)$/, '')
ok = ['.d.ts', '.d.mts', '.d.cts', '.ts', '.tsx'].some((x) =>
existsSync(noext + x),
) // no /index fallback
} else {
ok =
['.d.ts', '.ts', '.tsx'].some((x) => existsSync(abs + x)) ||
['/index.d.ts', '/index.ts'].some((x) => existsSync(abs + x))
IMPORT_RE.lastIndex = 0
let match
while ((match = IMPORT_RE.exec(src))) {
const specifier = match[1]
if (!resolves(file, specifier)) {
findings.push(`${specifier} <- ${relative(ROOT, file)}`)
}
if (!ok) findings.push(`${spec} <- ${file.replace(ROOT + '/', '')}`)
}
}
}

console.log(findings.sort().join('\n') || 'clean')
console.log(`\n${findings.length} dangling specifiers`)
const unique = [...new Set(findings)].sort()

if (findings.length > 0) {
process.exit(1)
if (unique.length === 0) {
console.log(
`scan-dangling-dts: clean (${filesScanned} .d.ts files across ${dists.length} package dist dirs)`,
)
process.exit(0)
}

console.error(
'scan-dangling-dts: dangling relative imports in published .d.ts:\n',
)
console.error(unique.join('\n'))
console.error(
`\n${unique.length} dangling specifier(s). Prefer concrete module paths (e.g. '../utils/client') or explicit '/index' so the declaration emit resolves under bundler/node16/nodenext.`,
)
process.exit(1)
Loading