From d1553d63efd3a2ed0667dfae12661b2e1d7b9217 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 09:34:50 -0500 Subject: [PATCH] fix(js,ts): of several `export default` lines, the one that names a node is the module's A template can hold statement-shaped text: react.dev's SandpackWithHTMLOutput.tsx writes two sandbox files' `export default function` lines above its own `export default memo(function ...)`, and reading only the first line left the module's default import with nothing. Every line-initial statement outside a block comment is now read, the first that names a node wins, and a function or class a statement declares binds only the node that starts there, so a template's `export default function formatHTML` can't reach the file's real formatHTML. The block-comment check reads the file once for all of its statements. Carries #2432's default-export tests unchanged (all pass), and adapts its multi-statement and declaration-position rules. Co-Authored-By: Claude Opus 5.5 --- __tests__/default-export-declaration.test.ts | 274 +++++++++++++++++++ __tests__/jsx-child-imports.test.ts | 28 ++ src/resolution/import-resolver.ts | 166 ++++++----- 3 files changed, 404 insertions(+), 64 deletions(-) create mode 100644 __tests__/default-export-declaration.test.ts diff --git a/__tests__/default-export-declaration.test.ts b/__tests__/default-export-declaration.test.ts new file mode 100644 index 00000000..ab39db1c --- /dev/null +++ b/__tests__/default-export-declaration.test.ts @@ -0,0 +1,274 @@ +/** + * A default import is the declaration the module's `export default` statement + * names, wherever it sits in the file — not the first exported function. + * + * A React Router 6.4+ data-router page exports its loader or action above the + * page component (`export function loader() {…}` then `export default function + * Vans() {…}`), and `import Vans, { loader as vansLoader } from './Vans'` bound + * `Vans` to `loader`: the route `/vans` rendered the loader, and every call + * through the default import went there too. A component file that exports a + * styled or memo component lost its default function or binding to that + * component, and an anonymous default (`export default function () {…}`, + * `export default () => …`) took whatever exported function came first — even + * one nested inside the default itself. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +const files: Record = { + 'package.json': JSON.stringify({ name: 'vanlife', private: true, dependencies: { react: '^18', 'react-router-dom': '^6.4' } }), + 'src/index.jsx': `import { RouterProvider, createBrowserRouter, createRoutesFromElements, Route } from "react-router-dom" +import Vans, { loader as vansLoader } from "./pages/Vans/Vans" +import HostVans, { loader as hostVansLoader } from "./pages/Host/HostVans" +import Login, { action as loginAction } from "./pages/Login" + +const router = createBrowserRouter(createRoutesFromElements( + + } action={loginAction} /> + } loader={vansLoader} /> + } loader={hostVansLoader} /> + +)) + +export default function App() { + return +} +`, + 'src/api.js': `export function getVans() { + return fetch("/api/vans") +} +`, + 'src/pages/Vans/Vans.jsx': `import { useLoaderData, defer } from "react-router-dom" +import { getVans } from "../../api" + +export function loader() { + return defer({ vans: getVans() }) +} + +export default function Vans() { + const dataPromise = useLoaderData() + function renderVanElements(vans) { + return vans.map((van) =>

{van.name}

) + } + return
{renderVanElements(dataPromise.vans)}
+} +`, + 'src/pages/Host/HostVans.jsx': `import { useLoaderData } from "react-router-dom" +import { getVans } from "../../api" + +export const loader = async () => getVans() + +export default function HostVans() { + return
    {useLoaderData().length}
+} +`, + 'src/pages/Login.jsx': `import { useActionData } from "react-router-dom" + +export async function action({ request }) { + return request.formData() +} + +export default function Login() { + const data = useActionData() + return
{data}
+} +`, + // A Next.js API route: a helper exported above the default handler. + 'src/api/vans.ts': `export function listVans() { + return [] +} + +export default async function handler(req: unknown, res: { json(v: unknown): void }) { + res.json(listVans()) +} +`, + 'src/store.ts': `export function createStore() { + return {} +} + +export default class Store { + read() { + return 1 + } +} +`, + // A regex with a backtick inside a template literal's interpolation + // (outline's headingToSlug): a string masker loses step there and blanked + // the statement below it. + 'src/slug.ts': `export function escapeHtml(text: string) { + return text +} + +function safeSlugify(text: string) { + return \`h-\${escapeHtml(text).replace(/[\`]/g, "")}\` +} + +export default function headingToSlug(text: string, index = 0) { + return index === 0 ? safeSlugify(text) : \`\${safeSlugify(text)}-\${index}\` +} +`, + // A line break between the statement and the declaration it writes. + 'src/stream.js': `export function emit() { + return 1 +} + +export default +function* stream() { + yield emit() +} +`, + // An exported styled component above the default function, or the binding. + 'src/components/Title.tsx': `import styled from "styled-components" + +export const Heading = styled.h1\` + font-size: 2rem; +\` + +export default function Title() { + return Vans +} +`, + 'src/components/Banner.tsx': `import styled from "styled-components" + +export const Strip = styled.div\` + display: flex; +\` + +const Banner = () => + +export default Banner +`, + // Anonymous defaults name no declaration of their own. + 'src/anonymous.js': `export function helper() { + return 1 +} + +export default function () { + function inner() { + return helper() + } + return inner() +} +`, + 'src/arrow.jsx': `export function useThing() { + return 1 +} + +export default () => { + const handle = () => useThing() + return