Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`,
bare `'fs'` and `require('node:fs')` bindings linked their calls to
same-named project functions. Main no longer does that for any static
form. PR 2120 (released in 1.6.2) declines a name match for a name the
file imports from outside the repository. PR 2437 made that hold in
projects with a catch-all tsconfig path alias, and PR 2671 extended it to
function values such as `promisify(readFile)`.
What still linked is the same binding written as a dynamic import. The
out-of-repo check reads the file's static import mappings only. The name
matcher deliberately treats `const { x } = await import('./m')` as an
import, not a local shadow, so the call fell through to a same-named
project function. vite's
`const { createServer } = await import('node:http'); return createServer(app)`
linked to vite's own `createServer`.
Fix (name-matcher.ts):
- isOutOfRepoBinding also asks dynamicImportSource for the module a
dynamic import binds the name from, where the reference sits.
- A per-file scan (FIFO of 64, cleared with the other name-matcher memos)
finds `const { a, b: c, default: d } = await import('x')` declarations
and `import('x').then(({ a }) => ...)` callbacks on the raw text. Only
files with a match are comment- and string-blanked, to drop matches in
comments and strings and to find where each binding's scope ends: the
enclosing block (a top-level await binds the whole module) or the
callback body.
- A binding counts only when the reference sits inside that scope after
it, and the innermost one wins, also over a static import of the name.
nitro's `async function createWebSocketProxy()` imports crossws's
`createWebSocketProxy` inside itself: only that inner call is dropped,
and the file's other calls still reach the local function.
- jsBlockEnd and jsBodyEnd are the forward forms of jsBlockOpenAtEnd and
jsBodyOpenAtEnd, which now call them.
Validation:
- __tests__/dynamic-import-binding.test.ts: 5 of 6 tests fail with main's
name-matcher.ts (the project-module control passes there); all 6 pass on
the kernel and with CODEGRAPH_KERNEL=0. Four mutations each fail their
test: no scope end, no callback form, no dynamic source, and no memo
clearing (the in-process sync test).
- Sync equals a fresh index (CLI) for adding and removing the dynamic
import, for the namesake file arriving later, and for the nitro shape.
- A/B on 9 TS/JS repos plus 2 controls, both arms with the kernel: 6 edges
removed, 0 added, 0 retargeted, all 6 wrong. vite has 3 (node:http
createServer, node:fs/promises readFile, rolldown watch), memfs 2
(node:util promisify), nitro 1 (the crossws self-loop). pathe, c12,
unbuild, fs-extra, unenv and nuxt are byte-identical, as are the flask
and gin controls.
- Cost: fresh index median of 3, interleaved: vite 9.17 s main vs 9.50 s,
nitro 7.43 s vs 7.12 s (noise on a shared box). The scan itself over
every vite source file takes 40 ms (24 files reach the blanking pass).
- Kernel parity is unchanged: 772/776 files byte-parity, 4 deferred,
over nitro/src and vite's src (this is a resolver-only change).
Fixes#2491
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>