Summary
Several packages emit dangling relative imports in their published .d.ts files — declaration imports that point at a file that doesn't exist. Consumers compile with skipLibCheck: true (the default in most app tsconfigs), which silently swallows these unresolved imports: the imported symbol resolves to any instead of erroring. That degrades public types without any signal.
This is the root cause of the bug fixed in #919: the Gemini adapters imported the utils directory barrel as '../utils', which the declaration build emitted as '../utils.js'. But utils builds to utils/index.js — no utils.js exists — so under bundler/node16/nodenext resolution the specifier doesn't resolve (an explicit .js extension is remapped to a sibling .d.ts and does not fall back to /index). GeminiClientConfig collapsed to any in consumers, dropping every inherited GoogleGenAIOptions field (httpOptions, apiVersion, vertexai, …) and producing a spurious `httpOptions` does not exist error at call sites.
#919 fixed the 6 main Gemini adapters. A full sweep shows the same class of bug exists in 10 packages (28 specifiers total), including one still-unfixed Gemini file (experimental/text-interactions/adapter.ts).
Confirmed findings
Scanning every built packages/*/dist/**/*.d.ts for relative imports whose target file doesn't exist:
### packages/ai (5)
../middleware.js <- activities/generateAudio/index.d.ts (real barrel: activities/middleware/)
../middleware.js <- activities/generateImage/index.d.ts
../middleware.js <- activities/generateSpeech/index.d.ts
../middleware.js <- activities/generateTranscription/index.d.ts
../middleware.js <- activities/generateVideo/index.d.ts
### packages/ai-anthropic (4)
../tools.js <- text/text-provider-options.d.ts
../utils.js <- adapters/summarize.d.ts
../utils.js <- adapters/text.d.ts
./tools.js <- index.d.ts
### packages/ai-bedrock (1)
./utils.js <- index.d.ts
### packages/ai-fal (6)
../utils.js <- adapters/audio.d.ts | image.d.ts | speech.d.ts | transcription.d.ts | video.d.ts
./utils.js <- index.d.ts
### packages/ai-gemini (1)
../../utils.js <- experimental/text-interactions/adapter.d.ts (missed by #919)
### packages/ai-grok (4)
../utils.js <- adapters/image.d.ts | summarize.d.ts | text.d.ts | video.d.ts
### packages/ai-groq (2)
../utils.js <- adapters/text.d.ts | tts.d.ts
### packages/ai-mistral (1)
../utils.js <- adapters/text.d.ts
### packages/ai-ollama (1)
./tools.js <- index.d.ts
### packages/ai-openrouter (3)
../utils.js <- adapters/image.d.ts
./tools.js <- index.d.ts
./utils.js <- index.d.ts
TOTAL: 28 dangling specifiers across 10 packages
Each one is a directory barrel (utils/, tools/, middleware/) imported bare and emitted with a .js extension that resolves to nothing. Whether it produces a visible symptom depends on what the barrel re-exports (a re-exported type that extends a third-party interface is the worst case — it becomes any and drops fields, exactly like GeminiClientConfig), but all 28 are latent public-API-type defects.
Proposed work
1. Fix all 28 dangling imports
Switch bare directory-barrel imports to concrete module paths so the emitted declaration resolves, matching the convention already used in ai-openai (import … from '../utils/client'). Per-package, low-risk (this is exactly what #919 did for the main Gemini adapters). Alternatively use explicit '../utils/index'.
2. Add a guardrail so it can't regress
skipLibCheck on the consumer side is why this went unnoticed, so we need a check on the producer side:
- Recommended — declaration-import lint over built
dist/**/*.d.ts. Deterministic, fast, no third-party node_modules noise; fails CI if any relative declaration import doesn't resolve to a real file (respecting the no-/index-fallback rule for explicit .js). A working scanner is below; it could become a test:dts Nx target or fold into test:build.
- Alternative — flip
skipLibCheck: false on an isolated declarations-only consumer project per package. Closer to "turn skipLibCheck off" but noisier: with it off, tsc also checks third-party dependency .d.ts, which routinely have their own errors — so it needs careful scoping (e.g. paths mapping only our packages) to avoid false positives.
- Also worth evaluating
@arethetypeswrong/cli as a published-types check under multiple resolution modes.
3. (Optional) fix it centrally upstream
The .js-for-a-directory emit comes from the shared @tanstack/vite-config declaration step. If that can emit '../utils/index.js' for directory barrels, the whole class is fixed at the source without touching each import. That package lives outside this repo, so per-package fixes (step 1) are the practical path here; note it as a follow-up.
Reproduction / scanner
// scan-dangling-dts.mjs — run from repo root after `nx run-many --targets=build --exclude='examples/**,testing/**'`
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
import { dirname, resolve, join } from 'node:path'
const ROOT = process.cwd()
function walk(dir, out = []) {
for (const e of readdirSync(dir)) {
const p = join(dir, e); const st = statSync(p)
if (st.isDirectory()) { if (e !== 'node_modules') walk(p, out) }
else if (e.endsWith('.d.ts')) out.push(p)
}
return out
}
const RE = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+)['"]/g
const findings = []
const dists = readdirSync(join(ROOT, 'packages'))
.map((p) => join(ROOT, 'packages', p, 'dist')).filter((d) => existsSync(d))
for (const dist of dists) for (const file of walk(dist)) {
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))
}
if (!ok) findings.push(`${spec} <- ${file.replace(ROOT + '/', '')}`)
}
}
console.log(findings.sort().join('\n') || 'clean')
console.log(`\n${findings.length} dangling specifiers`)
Acceptance criteria
Context: follow-up to #919.
Summary
Several packages emit dangling relative imports in their published
.d.tsfiles — declaration imports that point at a file that doesn't exist. Consumers compile withskipLibCheck: true(the default in most app tsconfigs), which silently swallows these unresolved imports: the imported symbol resolves toanyinstead of erroring. That degrades public types without any signal.This is the root cause of the bug fixed in #919: the Gemini adapters imported the
utilsdirectory barrel as'../utils', which the declaration build emitted as'../utils.js'. Bututilsbuilds toutils/index.js— noutils.jsexists — so underbundler/node16/nodenextresolution the specifier doesn't resolve (an explicit.jsextension is remapped to a sibling.d.tsand does not fall back to/index).GeminiClientConfigcollapsed toanyin consumers, dropping every inheritedGoogleGenAIOptionsfield (httpOptions,apiVersion,vertexai, …) and producing a spurious`httpOptions` does not existerror at call sites.#919 fixed the 6 main Gemini adapters. A full sweep shows the same class of bug exists in 10 packages (28 specifiers total), including one still-unfixed Gemini file (
experimental/text-interactions/adapter.ts).Confirmed findings
Scanning every built
packages/*/dist/**/*.d.tsfor relative imports whose target file doesn't exist:Each one is a directory barrel (
utils/,tools/,middleware/) imported bare and emitted with a.jsextension that resolves to nothing. Whether it produces a visible symptom depends on what the barrel re-exports (a re-exported type that extends a third-party interface is the worst case — it becomesanyand drops fields, exactly likeGeminiClientConfig), but all 28 are latent public-API-type defects.Proposed work
1. Fix all 28 dangling imports
Switch bare directory-barrel imports to concrete module paths so the emitted declaration resolves, matching the convention already used in
ai-openai(import … from '../utils/client'). Per-package, low-risk (this is exactly what #919 did for the main Gemini adapters). Alternatively use explicit'../utils/index'.2. Add a guardrail so it can't regress
skipLibCheckon the consumer side is why this went unnoticed, so we need a check on the producer side:dist/**/*.d.ts. Deterministic, fast, no third-partynode_modulesnoise; fails CI if any relative declaration import doesn't resolve to a real file (respecting the no-/index-fallback rule for explicit.js). A working scanner is below; it could become atest:dtsNx target or fold intotest:build.skipLibCheck: falseon an isolated declarations-only consumer project per package. Closer to "turn skipLibCheck off" but noisier: with it off, tsc also checks third-party dependency.d.ts, which routinely have their own errors — so it needs careful scoping (e.g.pathsmapping only our packages) to avoid false positives.@arethetypeswrong/clias a published-types check under multiple resolution modes.3. (Optional) fix it centrally upstream
The
.js-for-a-directory emit comes from the shared@tanstack/vite-configdeclaration step. If that can emit'../utils/index.js'for directory barrels, the whole class is fixed at the source without touching each import. That package lives outside this repo, so per-package fixes (step 1) are the practical path here; note it as a follow-up.Reproduction / scanner
Acceptance criteria
.d.tsimport (whichever guardrail we pick).ai-gemini/experimental/text-interactions/adapter.tsis fixed (the one fix(ai-gemini): resolve GeminiClientConfig from concrete module in adapter types #919 missed).Context: follow-up to #919.