From ba9d69e13a7f10bffa8711869f75d6960067c1be Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 00:47:41 -0500 Subject: [PATCH] fix(frameworks): a framework resolves only references written in its own languages Strategy 1 of resolveOneInner ran every detected framework's resolve() on every reference, whatever its language: `languages` gated only extraction, and gateFrameworkLanguage lets every `calls` result through. Express, detected in etcd by its file-content scan, linked Go `*zap.Logger` result types to a method named `Logger`, and in harbor and bitwarden it linked Python and Rust `validate()` / `authenticate()` calls to an unrelated method or to the calling function itself. getResolvingFrameworks() narrows the loop to the frameworks whose `resolveLanguages` (else `languages`) list the reference's language; one declaring neither stays universal. `resolveLanguages` lets a resolver read a language it extracts nothing from without running extract() there: Svelte reads TS/JS ($lib imports, runes in .svelte.ts), ASP.NET reads Razor (a page's `@model` is the PageModel beside it). claimsReference() stays universal: protobuf's leading-`::` C++ calls get past the name pre-filter only on the Swift/ObjC bridge's claim. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/framework-resolve-language.test.ts | 190 +++++++++++++++++++ docs/design/framework-coverage.md | 9 + src/resolution/frameworks/csharp.ts | 3 + src/resolution/frameworks/index.ts | 21 ++ src/resolution/frameworks/react.ts | 2 - src/resolution/frameworks/svelte.ts | 3 + src/resolution/frameworks/swift.ts | 6 +- src/resolution/index.ts | 31 ++- src/resolution/types.ts | 15 +- 10 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 __tests__/framework-resolve-language.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1144fe4..416628923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. +- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. - In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading. diff --git a/__tests__/framework-resolve-language.test.ts b/__tests__/framework-resolve-language.test.ts new file mode 100644 index 000000000..5751ce1f2 --- /dev/null +++ b/__tests__/framework-resolve-language.test.ts @@ -0,0 +1,190 @@ +/** + * A framework resolves only the references written in its own languages. + * Express is detected in etcd by a file-content scan, and its `logger` + * middleware rule (a same-file declaration named `logger`) took a Go method's + * `*zap.Logger` result type for the method itself. A resolver that reads more + * languages than it extracts from lists them: a SvelteKit `+page.server.ts` + * still resolves its `$lib/…` imports, and a Razor page's `@model` its + * PageModel. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { getResolvingFrameworks } from '../src/resolution/frameworks'; +import type { FrameworkResolver } from '../src/resolution/types'; + +function writeProject(prefix: string, files: Record): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + 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); + } + return root; +} + +describe('getResolvingFrameworks', () => { + const go: FrameworkResolver = { name: 'go', languages: ['go'], detect: () => true, resolve: () => null }; + const svelte: FrameworkResolver = { + name: 'svelte', languages: ['svelte'], resolveLanguages: ['svelte', 'typescript'], detect: () => true, resolve: () => null, + }; + const any: FrameworkResolver = { name: 'any', detect: () => true, resolve: () => null }; + + it('keeps the frameworks that list the language, and those that list none', () => { + expect(getResolvingFrameworks([go, svelte, any], 'go').map((f) => f.name)).toEqual(['go', 'any']); + expect(getResolvingFrameworks([go, svelte, any], 'python').map((f) => f.name)).toEqual(['any']); + }); + + it('reads resolveLanguages before languages', () => { + expect(getResolvingFrameworks([go, svelte, any], 'typescript').map((f) => f.name)).toEqual(['svelte', 'any']); + expect(getResolvingFrameworks([go, svelte, any], 'svelte').map((f) => f.name)).toEqual(['svelte', 'any']); + }); +}); + +describe('Express beside Go code (etcd)', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-go-', { + 'package.json': JSON.stringify({ name: 'dashboard', dependencies: { express: '^4.19.0' } }), + 'web/server.js': `const express = require('express'); +const app = express(); + +function logger(req, res, next) { + next(); +} + +app.use(logger); +app.get('/health', (req, res) => res.json({ ok: true })); +`, + 'go.mod': 'module example.com/kv\n\ngo 1.22\n', + 'server/read/read_test.go': `package read + +import "go.uber.org/zap" + +type mockServer struct { + lg *zap.Logger +} + +func (s *mockServer) Logger() *zap.Logger { return s.lg } +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('resolves no Go reference', () => { + const ids = cg.getNodesInFile('server/read/read_test.go').map((n) => n.id); + const byExpress = cg.getOutgoingEdgesFrom(ids).filter((e) => (e.metadata as { framework?: string } | undefined)?.framework === 'express'); + expect(byExpress).toEqual([]); + }); + + it('does not take a method’s `*zap.Logger` result for the method itself', () => { + const logger = cg.getNodesInFile('server/read/read_test.go').find((n) => n.kind === 'method' && n.name === 'Logger')!; + expect(logger).toBeDefined(); + expect(cg.getOutgoingEdges(logger.id).map((e) => e.target)).not.toContain(logger.id); + }); +}); + +describe('Svelte in a SvelteKit app', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-svelte-', { + 'package.json': JSON.stringify({ name: 'web', devDependencies: { '@sveltejs/kit': '^2.0.0', svelte: '^5.0.0' } }), + 'src/lib/api.ts': `export async function get(path: string) { + return fetch(path); +} +`, + 'src/routes/+page.server.ts': `import * as api from '$lib/api'; + +export async function load() { + return { articles: await api.get('/articles') }; +} +`, + 'src/routes/+page.svelte': ` + +

{data.articles}

+`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('still resolves a `$lib/…` import written in a .ts module', () => { + const ids = cg.getNodesInFile('src/routes/+page.server.ts').map((n) => n.id); + const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports').map((e) => cg.getNode(e.target)!); + expect(imported.map((n) => n.filePath)).toContain('src/lib/api.ts'); + }); +}); + +describe('ASP.NET in a Razor Pages app (eShopOnWeb)', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-razor-', { + 'Program.cs': `var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +var app = builder.Build(); +app.MapRazorPages(); +app.Run(); +`, + 'Pages/_ViewImports.cshtml': `@namespace Shop.Web.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +`, + 'Pages/Index.cshtml': `@page +@model IndexModel +

Catalog

+`, + 'Pages/Index.cshtml.cs': `using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Shop.Web.Pages; + +public class IndexModel : PageModel +{ +} +`, + 'Pages/Basket/Index.cshtml': `@page +@model IndexModel +

Basket

+`, + 'Pages/Basket/Index.cshtml.cs': `using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Shop.Web.Pages.Basket; + +public class IndexModel : PageModel +{ +} +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('still links each page’s `@model` to the PageModel beside it', () => { + const modelOf = (page: string) => { + const ids = cg.getNodesInFile(page).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.filePath); + }; + expect(modelOf('Pages/Index.cshtml')).toEqual(['Pages/Index.cshtml.cs']); + expect(modelOf('Pages/Basket/Index.cshtml')).toEqual(['Pages/Basket/Index.cshtml.cs']); + }); +}); diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md index c4a3765d2..043a424d1 100644 --- a/docs/design/framework-coverage.md +++ b/docs/design/framework-coverage.md @@ -283,6 +283,15 @@ Each of these cost real debugging time; they are not hypothetical. itself mounted under another file's `` reads as if at the root, and a navigation that failed while its route was gone is not retried until a full index. +14. **`resolve()` sees only references written in the framework's own + languages** (`getResolvingFrameworks`): Express's `logger` middleware rule + once took etcd's Go `*zap.Logger` result types for a method. A navigation + written in markup (`.svelte`, `.vue`) needs that language in `languages`. + A resolver that reads a language it extracts nothing from lists it in + `resolveLanguages` instead — Svelte's `$lib/…` imports in `.ts` route + modules, ASP.NET's Razor `@model` — because widening `languages` also runs + `extract()` on those files. `claimsReference()` is still asked of every + detected framework. --- diff --git a/src/resolution/frameworks/csharp.ts b/src/resolution/frameworks/csharp.ts index a70daa4a9..0cda2b67b 100644 --- a/src/resolution/frameworks/csharp.ts +++ b/src/resolution/frameworks/csharp.ts @@ -12,6 +12,9 @@ import { pickByNameAndKind } from './name-heuristic'; export const aspnetResolver: FrameworkResolver = { name: 'aspnet', languages: ['csharp'], + // A Razor page's `@model CheckoutModel` names the PageModel in the + // code-behind beside it (`Checkout.cshtml.cs`); these rules look there first. + resolveLanguages: ['csharp', 'razor'], detect(context: ResolutionContext): boolean { // Check for .csproj files with ASP.NET references diff --git a/src/resolution/frameworks/index.ts b/src/resolution/frameworks/index.ts index da76403c3..da8f7b0ec 100644 --- a/src/resolution/frameworks/index.ts +++ b/src/resolution/frameworks/index.ts @@ -142,6 +142,27 @@ export function getApplicableFrameworks( ); } +/** + * Filter a list of detected frameworks down to ones whose `resolve()` reads a + * reference written in `language`: its `resolveLanguages`, else its + * `languages`. Frameworks that declare neither are treated as universal. + * + * A framework's conventions are its languages': Express's `logger` middleware + * rule took a Go method's `*zap.Logger` result type for the method itself. + * Bridges list the language their references are written in — the React + * Native bridge JS, the Swift ↔ Objective-C bridge both, Drupal the + * `routing.yml` it reads controllers from. + */ +export function getResolvingFrameworks( + detected: FrameworkResolver[], + language: Language +): FrameworkResolver[] { + return detected.filter((fw) => { + const languages = fw.resolveLanguages ?? fw.languages; + return !languages || languages.includes(language); + }); +} + /** * Register a custom framework resolver */ diff --git a/src/resolution/frameworks/react.ts b/src/resolution/frameworks/react.ts index 72d05c91d..13520faec 100644 --- a/src/resolution/frameworks/react.ts +++ b/src/resolution/frameworks/react.ts @@ -22,8 +22,6 @@ export const reactResolver: FrameworkResolver = { // Includes 'tsx'/'jsx' so route extraction runs on JSX files (where // `}>` routes live) — without them the .tsx/.jsx grammars // were filtered out of the extract pass and those routes were never indexed. - // (resolve() is unaffected — it runs for every detected framework regardless - // of language; only the extract pass filters on `languages`.) languages: ['javascript', 'typescript', 'tsx', 'jsx'], detect(context: ResolutionContext): boolean { diff --git a/src/resolution/frameworks/svelte.ts b/src/resolution/frameworks/svelte.ts index 8a8495fc5..228c2248a 100644 --- a/src/resolution/frameworks/svelte.ts +++ b/src/resolution/frameworks/svelte.ts @@ -45,6 +45,9 @@ const SVELTEKIT_MODULE_PREFIXES = [ export const svelteResolver: FrameworkResolver = { name: 'svelte', languages: ['svelte'], + // Runes are written in `.svelte.ts` / `.svelte.js` modules too, and a + // SvelteKit `+page.ts` or `hooks.server.ts` imports `$lib/…` and `$app/…`. + resolveLanguages: ['svelte', 'typescript', 'javascript'], detect(context: ResolutionContext): boolean { // Check for svelte or @sveltejs/kit in package.json diff --git a/src/resolution/frameworks/swift.ts b/src/resolution/frameworks/swift.ts index 25e074a64..9ad2e95b3 100644 --- a/src/resolution/frameworks/swift.ts +++ b/src/resolution/frameworks/swift.ts @@ -41,7 +41,7 @@ export const swiftUIResolver: FrameworkResolver = { resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { // Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache - // : NSObject ` is no SwiftUI view or model (languages gates only extraction). + // : NSObject ` is no SwiftUI view or model. if (ref.language !== 'swift') return null; // Pattern 1: View references (SwiftUI views are PascalCase ending in View) if (ref.referenceName.endsWith('View') && /^[A-Z]/.test(ref.referenceName)) { @@ -110,7 +110,7 @@ export const uikitResolver: FrameworkResolver = { resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { // Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache - // : NSObject ` is no SwiftUI view or model (languages gates only extraction). + // : NSObject ` is no SwiftUI view or model. if (ref.language !== 'swift') return null; // Pattern 1: ViewController references if (ref.referenceName.endsWith('ViewController')) { @@ -201,7 +201,7 @@ export const vaporResolver: FrameworkResolver = { resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { // Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache - // : NSObject ` is no SwiftUI view or model (languages gates only extraction). + // : NSObject ` is no SwiftUI view or model. if (ref.language !== 'swift') return null; // Pattern 0: a route's handler — `use: SearchController.show` arrives as // `SearchController@show`, `use: self.index` / `use: index` as `@index`. diff --git a/src/resolution/index.ts b/src/resolution/index.ts index d8eca46e4..73230d4db 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -35,7 +35,7 @@ import { matchShopifyThemeFile } from './shopify-themes'; import { resolveViaImport, resolvePhpImportedStaticCall, resolvePhpQualifiedClassRef, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isDartImportRef, isLuaRequireRef, isJsPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver'; import { ResolverPool, minRefsForPool, shouldEngageAdaptively } from './resolver-pool'; import { resolveAliasBinding } from './alias-binding'; -import { detectFrameworks } from './frameworks'; +import { detectFrameworks, getResolvingFrameworks } from './frameworks'; import { synthesizeCallbackEdges } from './callback-synthesizer'; import { createYielder, type MaybeYield } from './cooperative-yield'; import { MAX_SOURCE_FILE_SIZE_BYTES } from '../file-limits'; @@ -235,6 +235,8 @@ export class ReferenceResolver { private queries: QueryBuilder; private context: ResolutionContext; private frameworks: FrameworkResolver[] = []; + /** `frameworks` narrowed to those that resolve each reference language (see getResolvingFrameworks). */ + private frameworksByLanguage = new Map(); // Chained static-factory/fluent call refs the first pass couldn't resolve, // collected in-memory and left pending in the DB until the post-pass // finishes, so a restart can recover the queue (#1577). Drained by @@ -359,9 +361,20 @@ export class ReferenceResolver { */ initialize(): void { this.frameworks = detectFrameworks(this.context); + this.frameworksByLanguage.clear(); this.clearCaches(); } + /** The detected frameworks whose `resolve()` reads a reference written in `language`. */ + private frameworksFor(language: Language): FrameworkResolver[] { + let frameworks = this.frameworksByLanguage.get(language); + if (!frameworks) { + frameworks = getResolvingFrameworks(this.frameworks, language); + this.frameworksByLanguage.set(language, frameworks); + } + return frameworks; + } + /** * Run each framework resolver's cross-file finalization pass and persist * the returned node updates. Idempotent — safe to call after every indexAll @@ -1279,6 +1292,10 @@ export class ReferenceResolver { // calls `FormatPrice`, which the exact-name set never lists. (CASE_INSENSITIVE_LANGUAGES.has(ref.language) && this.hasAnyPossibleMatchIgnoringCase(existenceName)) || this.matchesAnyImport(ref) || + // Every detected framework's claim, not only those that resolve this + // language: the check above reads no leading `::`, and protobuf's C++ + // `::_pbi::…` calls get past it only on the Swift ↔ Objective-C + // bridge's claim of any name with a `:` in it. this.frameworks.some((f) => f.claimsReference?.(ref.referenceName)); if (this.profileStages) this.stageAdd('preFilter', ref, preFilterPass, tPre); if (!preFilterPass) { @@ -1354,12 +1371,16 @@ export class ReferenceResolver { const candidates: ResolvedRef[] = []; - // Strategy 1: Try framework-specific resolution. Cross-language bridges - // are deliberately preserved (Drupal `routing.yml` → PHP controller, RN - // JS → native `calls`); other code references obey the shared family gate. + // Strategy 1: Try framework-specific resolution, by the frameworks that + // read this reference's language: Express's `logger` middleware rule took + // a Go method's `*zap.Logger` result for the method itself (etcd). + // Cross-language bridges are deliberately preserved (Drupal `routing.yml` + // → PHP controller, RN JS → native `calls`); each lists the language its + // references are written in. Other code references obey the shared + // family gate. const tFw = this.profileStages ? process.hrtime.bigint() : 0n; let fwEarly: ResolvedRef | null = null; - for (const framework of this.frameworks) { + for (const framework of this.frameworksFor(ref.language)) { const resolved = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref); // Name the resolver on the edge (`metadata.framework`): a Swift→ObjC or // React Native bridge hop says how it got into the graph, as a diff --git a/src/resolution/types.ts b/src/resolution/types.ts index 7eb6dead9..464498d83 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -257,8 +257,21 @@ export interface CrossFileNodes extends FrameworkExtractionResult { export interface FrameworkResolver { /** Framework name */ name: string; - /** Languages this framework applies to. If omitted, applies to all languages. */ + /** + * Languages this framework applies to: `extract()` runs only on files in + * them, and `resolve()` sees only references written in them (unless + * `resolveLanguages` says otherwise). If omitted, applies to all languages. + */ languages?: Language[]; + /** + * The languages whose references `resolve()` sees, when they are not + * `languages`. For a resolver that reads references in languages it + * extracts nothing from: SvelteKit's `$lib/…` imports and Svelte 5 runes + * are written in `.ts` / `.js` modules too, but only a `.svelte` file holds + * a route. Widening `languages` instead would run `extract()` on those + * files. + */ + resolveLanguages?: readonly Language[]; /** * Packages an app declares when it is built on this framework. When set, * `extract()` runs only on files of an app whose package.json — the file's