Commit Graph
1 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 7180fd0666 fix(js,ts): a dynamic import from Node or a package no longer links to a project namesake (#2788)
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>
2026-10-22 07:10:12 +00:00