mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(js,ts): a default import is what export default declares, not the first exported function (#2432)
A default import resolved to the module's first exported function or
component whenever the `export default` statement wrote a declaration
(`export default function Vans()`, `export default class Store`): only a
separate `export default NAME` statement was read. A React Router 6.4+
data-router page exports its `loader` or `action` above the page
component, so `import Vans, { loader as vansLoader } from './Vans'` bound
the route `/vans` (and every call through `Vans`) to `loader`.
The resolver now reads the file's own `export default` statement from
comment-stripped code. A declaration it writes is found by position, a
binding it names by name, and an anonymous function, class or arrow
function is no node at all (the guess took an exported function beside it,
or one nested inside it). The statement also beats an exported styled/memo
component above it. Every line-initial statement is read and the first one
that names a node of the file wins, so template text that only reads like
a statement names nothing. Strings are not masked: a masker loses step on a
template whose `${…}` holds another template or a regex with a backtick,
and on outline it blanked the real statement below one. Only an expression
default (`memo(Card)`, `new Service()`), a binding the file doesn't
declare, or no statement keeps the old guess.
`require('./x').default` (#2433) reads the same statement first, so it
reaches the declared default too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b8b3eb9778
commit
33c1436130
@@ -111,6 +111,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div<WrapperProps>` followed by its CSS, or `styled(Base)<Props>`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `<Text>` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading.
|
||||
- `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 `<Route path="/docs" element={<Docs />} />`, 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 `<Team />`, 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. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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(
|
||||
<Route path="/">
|
||||
<Route path="login" element={<Login />} action={loginAction} />
|
||||
<Route path="vans" element={<Vans />} loader={vansLoader} />
|
||||
<Route path="host/vans" element={<HostVans />} loader={hostVansLoader} />
|
||||
</Route>
|
||||
))
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
`,
|
||||
'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) => <h3 key={van.id}>{van.name}</h3>)
|
||||
}
|
||||
return <div>{renderVanElements(dataPromise.vans)}</div>
|
||||
}
|
||||
`,
|
||||
'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 <ul>{useLoaderData().length}</ul>
|
||||
}
|
||||
`,
|
||||
'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 <form>{data}</form>
|
||||
}
|
||||
`,
|
||||
// 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 <Heading>Vans</Heading>
|
||||
}
|
||||
`,
|
||||
'src/components/Banner.tsx': `import styled from "styled-components"
|
||||
|
||||
export const Strip = styled.div\`
|
||||
display: flex;
|
||||
\`
|
||||
|
||||
const Banner = () => <Strip />
|
||||
|
||||
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 <button onClick={handle} />
|
||||
}
|
||||
`,
|
||||
// An expression default keeps the class it instantiates.
|
||||
'src/service.ts': `export class Service {
|
||||
run() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service()
|
||||
`,
|
||||
'src/use.tsx': `import handler from "./api/vans"
|
||||
import Store from "./store"
|
||||
import headingToSlug from "./slug"
|
||||
import stream from "./stream"
|
||||
import Title from "./components/Title"
|
||||
import Banner from "./components/Banner"
|
||||
import runAnonymous from "./anonymous"
|
||||
import Arrow from "./arrow"
|
||||
import service from "./service"
|
||||
|
||||
export function consume() {
|
||||
handler({}, { json() {} })
|
||||
new Store().read()
|
||||
headingToSlug("Vans")
|
||||
stream()
|
||||
Title()
|
||||
Banner()
|
||||
runAnonymous()
|
||||
Arrow()
|
||||
service.run()
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-default-export-'));
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `file:name` of every node `consume` calls or instantiates. */
|
||||
function consumed(): string[] {
|
||||
const consume = cg.getNodesInFile('src/use.tsx').find((n) => n.name === 'consume')!;
|
||||
return cg
|
||||
.getOutgoingEdges(consume.id)
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
|
||||
.map((e) => cg.getNode(e.target)!)
|
||||
.map((n) => `${n.filePath}:${n.name}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('a default import of a module that exports something above its default', () => {
|
||||
it('binds each data-router route to the page component, not its loader or action', () => {
|
||||
const bindings = cg
|
||||
.getNodesByKind('route')
|
||||
.flatMap((r) =>
|
||||
cg
|
||||
.getOutgoingEdges(r.id)
|
||||
.filter((e) => e.kind === 'references')
|
||||
.map((e) => `${r.name} -> ${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`)
|
||||
)
|
||||
.sort();
|
||||
expect(bindings).toEqual([
|
||||
'/host/vans -> src/pages/Host/HostVans.jsx:HostVans',
|
||||
'/login -> src/pages/Login.jsx:Login',
|
||||
'/vans -> src/pages/Vans/Vans.jsx:Vans',
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls the default-exported function or class the statement declares', () => {
|
||||
const targets = consumed();
|
||||
expect(targets).toContain('src/api/vans.ts:handler');
|
||||
expect(targets).toContain('src/store.ts:Store');
|
||||
expect(targets).toContain('src/slug.ts:headingToSlug');
|
||||
expect(targets).toContain('src/stream.js:stream');
|
||||
expect(targets).not.toContain('src/api/vans.ts:listVans');
|
||||
expect(targets).not.toContain('src/store.ts:createStore');
|
||||
expect(targets).not.toContain('src/slug.ts:escapeHtml');
|
||||
expect(targets).not.toContain('src/stream.js:emit');
|
||||
});
|
||||
|
||||
it('prefers what the statement exports to an exported component above it', () => {
|
||||
const targets = consumed();
|
||||
expect(targets).toContain('src/components/Title.tsx:Title');
|
||||
expect(targets).toContain('src/components/Banner.tsx:Banner');
|
||||
expect(targets).not.toContain('src/components/Title.tsx:Heading');
|
||||
expect(targets).not.toContain('src/components/Banner.tsx:Strip');
|
||||
});
|
||||
|
||||
it('binds an anonymous default to nothing, not to an exported function beside or inside it', () => {
|
||||
const targets = consumed();
|
||||
for (const wrong of ['src/anonymous.js:helper', 'src/anonymous.js:inner', 'src/arrow.jsx:useThing', 'src/arrow.jsx:handle']) {
|
||||
expect(targets).not.toContain(wrong);
|
||||
}
|
||||
});
|
||||
|
||||
it('still finds the class behind an expression default', () => {
|
||||
expect(consumed()).toContain('src/service.ts:run');
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { applyAliases } from './path-aliases';
|
||||
import { extractLocalExportAliases } from './alias-binding';
|
||||
import { resolveWorkspaceImport } from './workspace-packages';
|
||||
import { stripCommentsForRegex } from './strip-comments';
|
||||
import { makeLineAt } from './synth-utils';
|
||||
import { dartDirectiveFile } from './dart-libraries';
|
||||
import { moduleTail } from '../db/reference-tail';
|
||||
import {
|
||||
@@ -84,8 +85,9 @@ const exportedSymbolMemos = new WeakMap<ResolutionContext, Map<string, Node | un
|
||||
* Built from the file's exported rows alone: every worker of the resolver
|
||||
* pool builds its own index for each file an import reaches, and decoding
|
||||
* whole files there cost more than resolving through them. The two answers
|
||||
* that need more — the default-export binding and names a local export
|
||||
* clause introduces — read the source and name-targeted rows on first use.
|
||||
* that need more — what the `export default` statement exports and names a
|
||||
* local export clause introduces — read the source and name-targeted rows on
|
||||
* first use.
|
||||
*/
|
||||
interface FileExportIndex {
|
||||
/** Exported declarations by name. Read through {@link exportedByName}. */
|
||||
@@ -93,15 +95,11 @@ interface FileExportIndex {
|
||||
defaultComponent: Node | undefined;
|
||||
defaultFnClass: Node | undefined;
|
||||
/**
|
||||
* The node an `export default NAME` statement names, exported at its
|
||||
* declaration or not — the precise answer where `defaultFnClass` is a
|
||||
* guess. `const Home = () => …; export default Home` and the namespace
|
||||
* object `const UploadApi = { uploadARCapture }; export default UploadApi`
|
||||
* are both invisible to the `isExported` index above: neither declaration
|
||||
* has an `export_statement` ancestor. `undefined` until first read through
|
||||
* {@link defaultExportBindingNode}; `null` when there is none.
|
||||
* What the file's own `export default` statement exports — the precise
|
||||
* answer where `defaultComponent`/`defaultFnClass` are guesses. `undefined`
|
||||
* until first read through {@link statedDefaultExport}.
|
||||
*/
|
||||
defaultBinding?: Node | null;
|
||||
statedDefault?: StatedDefault;
|
||||
/**
|
||||
* Names a local export clause (`export { impl as alias }`) binds to a
|
||||
* declaration the extractor never flagged isExported, for names not in
|
||||
@@ -111,17 +109,115 @@ interface FileExportIndex {
|
||||
}
|
||||
|
||||
const DEFAULT_BINDING_KINDS = new Set<string>(['function', 'class', 'component', 'constant', 'variable']);
|
||||
const DEFAULT_EXPORT_BINDING_RE = /^[ \t]*export\s+default\s+([A-Za-z_$][\w$]*)\s*;?[ \t]*$/m;
|
||||
const DEFAULT_DECLARATION_KINDS = new Set<string>(['function', 'class', 'component']);
|
||||
/** Any `export default …` statement: the module is an ES module with a default export. */
|
||||
const ESM_DEFAULT_EXPORT_RE = /^[ \t]*export\s+default\b/m;
|
||||
const JS_FAMILY_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
||||
|
||||
/** The identifier `export default NAME` names in a JS-family file, or null. */
|
||||
function defaultExportBinding(filePath: string, context: ResolutionContext): string | null {
|
||||
if (!JS_FAMILY_FILE.test(filePath)) return null;
|
||||
const source = context.readFile(filePath);
|
||||
if (!source || !source.includes('export default')) return null;
|
||||
return source.match(DEFAULT_EXPORT_BINDING_RE)?.[1] ?? null;
|
||||
/**
|
||||
* What a file's own `export default` statement exports: the node it declares
|
||||
* or names, `'anonymous'` for a function or class with no name (no node
|
||||
* stands for it), `'unstated'` when the file says nothing a node answers.
|
||||
*/
|
||||
type StatedDefault = Node | 'anonymous' | 'unstated';
|
||||
|
||||
const EXPORT_DEFAULT_HEAD = /^[ \t]*export\s+default\s+/gm;
|
||||
/** `[async] function[*] [NAME]` / `[abstract] class [NAME]`; the name is group 1 or 2. */
|
||||
const DEFAULT_DECLARATION_RE =
|
||||
/^(?:async\s+)?function\b\s*(?:\*\s*)?([A-Za-z_$][\w$]*)?|^(?:abstract\s+)?class\b(?:\s+(?!extends\b|implements\b)([A-Za-z_$][\w$]*))?/;
|
||||
/** `[async] x =>`, or `[async] (` opening what may be an arrow function's parameters. */
|
||||
const DEFAULT_ARROW_RE = /^(?:async\s+)?(?:[A-Za-z_$][\w$]*\s*=>|\()/;
|
||||
/** `NAME` alone on its line. */
|
||||
const DEFAULT_BINDING_RE = /^([A-Za-z_$][\w$]*)\s*;?[ \t]*(?:\r?\n|$)/;
|
||||
|
||||
/**
|
||||
* The value of the `export default` statement whose value starts at `at`, in
|
||||
* comment-stripped code: the name a declaration or a binding gives it,
|
||||
* `'anonymous'` for a function, class or arrow function without one, or null
|
||||
* for any other expression.
|
||||
*/
|
||||
function readDefaultExport(code: string, at: number): { name: string; nameAt: number; declared: boolean } | 'anonymous' | null {
|
||||
const rest = code.slice(at, at + 2048);
|
||||
const decl = DEFAULT_DECLARATION_RE.exec(rest);
|
||||
if (decl) {
|
||||
const name = decl[1] ?? decl[2];
|
||||
return name ? { name, nameAt: at + decl[0].length - name.length, declared: true } : 'anonymous';
|
||||
}
|
||||
const arrow = DEFAULT_ARROW_RE.exec(rest);
|
||||
if (arrow) {
|
||||
if (!arrow[0].endsWith('(')) return 'anonymous';
|
||||
// A parenthesized list is an arrow function's parameters when `=>` or a
|
||||
// return type (`(…): JSX.Element =>`) follows it; otherwise it is an
|
||||
// expression (`(Foo)`).
|
||||
let depth = 0;
|
||||
for (let i = arrow[0].length - 1; i < rest.length; i++) {
|
||||
if (rest[i] === '(') depth++;
|
||||
else if (rest[i] === ')' && --depth === 0) return /^\s*(?:=>|:)/.test(rest.slice(i + 1)) ? 'anonymous' : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const binding = DEFAULT_BINDING_RE.exec(rest);
|
||||
return binding ? { name: binding[1]!, nameAt: at, declared: false } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `filePath`'s own `export default` statement exports. The statement
|
||||
* says it outright, where the export index can only guess:
|
||||
* - `export default function Vans() {…}` / `export default class Store` —
|
||||
* the declaration the statement writes, found by position. A React Router
|
||||
* data-router page exports its `loader` or `action` above it, and the
|
||||
* first-exported guess bound the page's default import to the loader.
|
||||
* - `export default Home` — the binding it names, exported at its
|
||||
* declaration or not: `const Home = () => …` and the namespace object
|
||||
* `const UploadApi = { uploadARCapture }` have no `export_statement`
|
||||
* ancestor, so the `isExported` index never sees them.
|
||||
* - `export default function () {…}`, `export default class {…}`, `export
|
||||
* default () => …` — anonymous: no node is the default export, and any
|
||||
* exported function beside it (or nested in it, which the extractor flags
|
||||
* exported too) would be the wrong one.
|
||||
* Anything else — an expression (`memo(Card)`, `new Service()`, `{ … }`), a
|
||||
* binding the file doesn't declare, no statement at all, a non-JS file — is
|
||||
* `'unstated'`. Comments are stripped first. Strings are not masked: a
|
||||
* template whose `${…}` holds another template, or a regex with a backtick,
|
||||
* sets a masker out of step, and it blanked the real statement after it
|
||||
* (outline's 2,900-line styled-components `Styles.ts`). A statement must
|
||||
* name a node of the file instead — one a template only writes as text
|
||||
* names none — and of several statements, one that does wins.
|
||||
*/
|
||||
function statedDefaultExport(filePath: string, idx: FileExportIndex, context: ResolutionContext): StatedDefault {
|
||||
if (idx.statedDefault !== undefined) return idx.statedDefault;
|
||||
let stated: StatedDefault | undefined;
|
||||
const source = JS_FAMILY_FILE.test(filePath) ? context.readFile(filePath) : null;
|
||||
if (source && /\bexport\s+default\b/.test(source)) {
|
||||
const code = stripCommentsForRegex(source, 'typescript');
|
||||
const lineAt = makeLineAt(code, 1);
|
||||
EXPORT_DEFAULT_HEAD.lastIndex = 0;
|
||||
for (let m = EXPORT_DEFAULT_HEAD.exec(code); m !== null; m = EXPORT_DEFAULT_HEAD.exec(code)) {
|
||||
const value = readDefaultExport(code, m.index + m[0].length);
|
||||
if (value === 'anonymous') {
|
||||
stated ??= 'anonymous';
|
||||
continue;
|
||||
}
|
||||
stated ??= 'unstated';
|
||||
if (value === null) continue;
|
||||
const named = nodesInFileNamed(filePath, value.name, context);
|
||||
// A declaration starts on the statement's line, or on its name's when
|
||||
// the statement breaks before it (`export default\nfunction X`).
|
||||
const from = lineAt(m.index);
|
||||
const to = lineAt(value.nameAt);
|
||||
const node = value.declared
|
||||
? named.find((n) => DEFAULT_DECLARATION_KINDS.has(n.kind) && n.startLine >= from && n.startLine <= to)
|
||||
: named
|
||||
.filter((n) => DEFAULT_BINDING_KINDS.has(n.kind))
|
||||
.sort((a, b) => a.startLine - b.startLine || a.startColumn - b.startColumn)[0];
|
||||
if (node) {
|
||||
stated = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx.statedDefault = stated ?? 'unstated';
|
||||
return idx.statedDefault;
|
||||
}
|
||||
const fileExportIndexes = new WeakMap<ResolutionContext, Map<string, FileExportIndex>>();
|
||||
|
||||
@@ -168,27 +264,19 @@ function nodesInFileNamed(filePath: string, name: string, context: ResolutionCon
|
||||
return context.getNodesInFileNamed?.(filePath, name) ?? context.getNodesInFile(filePath).filter((n) => n.name === name);
|
||||
}
|
||||
|
||||
/** The declaration `export default NAME` names in this file (see FileExportIndex.defaultBinding). */
|
||||
function defaultExportBindingNode(filePath: string, idx: FileExportIndex, context: ResolutionContext): Node | undefined {
|
||||
if (idx.defaultBinding === undefined) {
|
||||
const bound = defaultExportBinding(filePath, context);
|
||||
idx.defaultBinding =
|
||||
bound === null
|
||||
? null
|
||||
: (nodesInFileNamed(filePath, bound, context)
|
||||
.filter((n) => DEFAULT_BINDING_KINDS.has(n.kind))
|
||||
.sort((a, b) => a.startLine - b.startLine || a.startColumn - b.startColumn)[0] ?? null);
|
||||
}
|
||||
return idx.defaultBinding ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The declaration a module default-exports: the component a single-file
|
||||
* component file is, else what an `export default NAME` statement names, else
|
||||
* the first exported function or class (`export default class Foo`).
|
||||
* The declaration a module default-exports: what its own `export default`
|
||||
* statement declares or names (see {@link statedDefaultExport}), and nothing
|
||||
* for an anonymous default. Only a default the statement leaves unnamed
|
||||
* (`export default memo(Card)`, `new Service()`), or a module with no
|
||||
* statement, falls back: to the component a single-file component file is,
|
||||
* else to the first exported function or class.
|
||||
*/
|
||||
function esmDefaultExport(filePath: string, idx: FileExportIndex, context: ResolutionContext): Node | undefined {
|
||||
return idx.defaultComponent ?? defaultExportBindingNode(filePath, idx, context) ?? idx.defaultFnClass;
|
||||
const stated = statedDefaultExport(filePath, idx, context);
|
||||
if (stated === 'anonymous') return undefined;
|
||||
if (stated !== 'unstated') return stated;
|
||||
return idx.defaultComponent ?? idx.defaultFnClass;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2709,14 +2797,14 @@ function findExportedSymbolWalk(
|
||||
|
||||
// 1. Direct hit: the symbol is declared in this file.
|
||||
if (want.isDefault) {
|
||||
// What the file's own `export default` statement exports beats any
|
||||
// guess; an anonymous default is no node at all, and no neighbour is it.
|
||||
if (statedDefaultExport(filePath, exportIndex, context) === 'anonymous') return undefined;
|
||||
// Svelte/Vue single-file components ARE the module's default export,
|
||||
// but are extracted as kind 'component' (not function/class). Prefer
|
||||
// the component node; fall back to an exported function/class for the
|
||||
// `.ts`/`.tsx` `export default fn`/`class` case. Without the component
|
||||
// branch, an `export { default as X } from './X.svelte'` barrel never
|
||||
// resolves and the component shows a false 0 callers (#629).
|
||||
// A component file IS its default export; otherwise the statement that
|
||||
// names the binding beats the first-exported-function guess.
|
||||
// but are extracted as kind 'component' (not function/class), so the
|
||||
// fallback prefers the component node. Without it, an `export { default
|
||||
// as X } from './X.svelte'` barrel never resolves and the component shows
|
||||
// a false 0 callers (#629).
|
||||
const direct = esmDefaultExport(filePath, exportIndex, context);
|
||||
if (direct) return direct;
|
||||
// CommonJS: `module.exports = createApplication`, or `= require('./lib/express')`.
|
||||
|
||||
Reference in New Issue
Block a user