From ed6b88e53a4247316829a2e7f2d0d60202abce86 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Fri, 30 Oct 2026 12:55:21 +0000 Subject: [PATCH] fix(js): an export default line inside a template literal no longer names the default (#3068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit statedDefaultExport reads a JS/TS file's own export default statement by scanning comment-stripped source for line-initial `export default` heads. Strings are deliberately not masked there (a masker that loses step on nested templates blanked real statements), and a head counted when it named a node of the file, the first such head winning. A code generator's template text breaks that: in export function App() {} export function Main() {} export const template = ` export default App `; export default Main; the template's line names the real App, so `import X from './gen'` bound to App instead of Main (issue #2968). A template holding `export default () => …` also made a module whose real default is an expression read as anonymous, which keeps every guess away. templateLiteralTextAt (strip-comments.ts) scans the source once for template-literal text: it follows `${…}` nesting (a template inside an interpolation, a brace inside a regex there), and skips comments, quoted strings and regex literals, which may hold a backtick; a `/` after a value divides, anywhere else it opens a regex. A head inside template text is now skipped. When some template never closes the scan vouches for nothing and every head counts, as before. Nothing else changes: a head outside template text is read exactly as it was. Validation: default-export-declaration.test.ts gains the issue's file, a template returned by a function behind a nested template, one passed at module scope and one in a class field, a template's anonymous default above an expression default, and controls (a regex and a string holding a backtick, a division after a parenthesis, and a file whose scan can't close a template); strip-comments.test.ts tests the scan directly. Red with main's resolver: 2 of 10 failed; green: all pass. Checked against tree-sitter on every line-initial head of 4,191 JS/TS files in 16 corpora (outline, excalidraw, vben, bitwarden apps, mastodon, shadcn docs, zod, bulletproof-react, express, astro, react, trezor-suite, this repo's src, tests and ui): of 4,149 real export default statements the scan puts none inside template text, and all 322 heads it does place there are template text. A/B on excalidraw d3a9c2a1, vben 61c07936, astro e3bec3d2 (packages/astro/src and packages/integrations), with gin-gonic/gin 0f09c3a9 and psf/requests 3c7baaa0 as controls, both arms on main 4b0f72b7 through the native kernel (and again on 66449255 through wasm): all six byte-identical, since no default import there is decided by template text; index time unchanged. Not in this change: hasEsmDefaultExport, which only gates require('./x').default, still reads the raw text. Reimplements PR #3042 by @johnhldiver-beep. Fixes #2968 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/default-export-declaration.test.ts | 117 +++++++++++++++++++ __tests__/strip-comments.test.ts | 37 +++++- src/resolution/import-resolver.ts | 12 +- src/resolution/strip-comments.ts | 87 ++++++++++++++ 5 files changed, 250 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac94fef2..ae7d0872c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -351,6 +351,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind. - `codegraph sync` and the file watcher now link a component to the JSX that renders it, like ``, when the component is added in a file with no JSX of its own, as in `export default function Team() { return null; }`. Before, the link waited for an unrelated change to a file with JSX, or for a re-index, so the new component looked unused. - In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. A module loaded with `require('./x').default` is read the same way. Re-index JavaScript and TypeScript projects after upgrading. +- In JavaScript and TypeScript, a line like `export default App` written inside a template literal, as code generators and build plugins write the modules they produce, no longer counts as the module's own default export. Before, when that text named a function the file declares, a default import of the module linked to that function instead of the one the real `export default` statement names, and an unnamed `export default () => …` in such text could leave the default import linked to nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and @johnhldiver-beep for the first fix. (#2968) - In React and Vue, a tag for a component the file imports from a package, like `