Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript and JavaScript, a call on a value another file exports, like `store.add(…)` after `import { store } from './state'` with `export const store = new Store()` there, now links to the method of the class that file imports or declares when other classes in the project share its name, in Vue, Svelte and Astro components too. Before, it linked to whichever of them was indexed first: in vscode, calls on `FileAccess`, `extUri`, `errorHandler` and `MenuRegistry` reached the copies the Copilot extension vendors or one of its test fixtures, and in folo the mobile app's `settingSyncQueue.init()` reached the desktop app's `SettingSyncQueue`. Re-index TypeScript and JavaScript projects after upgrading.
- In TypeScript, a call through a field that a wrapper sets up, like `this._onDidChange.fire()` after `private readonly _onDidChange = this._register(new Emitter<void>())` or `= this._store.add(new Emitter<void>())`, now links to the method of the class handed to the wrapper, also in a class that inherits the field. Before, such a field had no type, so its calls linked nowhere, and that is one of the most common ways vscode declares its event emitters and disposables. A wrapper counts only when its own declaration hands back what it is given, like `_register<T>(o: T): T`, so a field set from a wrapper that returns something else, or from a member of what it returns (`.event`), is never taken for the class. A call through an inherited field is also found now when the field is declared further up the class hierarchy than before. Re-index TypeScript projects after upgrading.
- In TypeScript and JavaScript, a class whose declaration constrains a type parameter, like `class EditorPane<MementoType extends object = object> extends Composite<MementoType>`, is now read as extending the class after its type parameters. Before, the constraint's type was taken for its base class, so fields, wrapped event emitters and static methods inherited through such a class were missed or found on the wrong class. Class declarations that span several lines or sit below a decorator are read too. Re-index TypeScript and JavaScript projects after upgrading.
- In JavaScript and TypeScript, an import written from a jsconfig or tsconfig `baseUrl`, like `import MainLayout from 'layout/MainLayout'` under `"baseUrl": "src"`, or through a catch-all `"paths": { "*": ["./src/*"] }`, now links to the file it names, read with the config of the app the importing file belongs to, and so does an import of a single name like `'config'` or `'themes'`. Create React App and Vite admin templates such as Berry, Mantis and MatX write nearly every import this way. Before, each one was taken for an npm package, so it linked nothing, its names were matched by name alone, which could reach a same-named component in another app of the same repository, and a React Router route whose page loads through `lazy(() => import('views/dashboard/Default'))` pointed at that loader instead of the page and its layout. A package the app's `package.json` declares, or a Node built-in, never links to a project module of the same name, only to type declarations the project keeps for it, and only a file the index holds counts, never a folder or something in `node_modules`. `codegraph sync` and the file watcher link such an import too when the file it names is added later or gains what it imports. Re-index JavaScript and TypeScript projects after upgrading.

## [1.6.2] - 2026-10-03

Expand Down
447 changes: 447 additions & 0 deletions __tests__/tsconfig-baseurl-imports.test.ts

Large diffs are not rendered by default.

64 changes: 63 additions & 1 deletion __tests__/tsconfig-extends-aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
* - the nearest config wins: a child's own `paths` replaces (not merges with)
* the parent's, which is what `tsc` does
* - a cyclic `extends` terminates instead of blowing the stack
* - a `baseUrl` with no `paths` still loads, and only a declared `baseUrl`
* reads a bare specifier from its directory
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { loadProjectAliases, applyAliases } from '../src/resolution/path-aliases';
import { loadProjectAliases, applyAliases, aliasAndBaseUrlCandidates } from '../src/resolution/path-aliases';

function write(file: string, content: unknown): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
Expand Down Expand Up @@ -180,3 +182,63 @@ describe('tsconfig `extends` chains (#1534)', () => {
expect(loadProjectAliases(root)).toBeNull();
});
});

describe('a baseUrl with no paths', () => {
let root: string;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-tsbaseurl-'));
});

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});

it('loads a jsconfig that sets only baseUrl (CRA / Vite admin templates)', () => {
write(path.join(root, 'jsconfig.json'), { compilerOptions: { baseUrl: 'src' } });

const aliases = loadProjectAliases(root);
expect(aliases).not.toBeNull();
expect(aliases!.patterns).toEqual([]);
expect(aliases!.declaresBaseUrl).toBe(true);
expect(aliasAndBaseUrlCandidates('layout/MainLayout', aliases!, root)).toEqual(['src/layout/MainLayout']);
});

it('takes a baseUrl inherited through extends, relative to the file that set it', () => {
write(path.join(root, 'config/tsconfig.base.json'), { compilerOptions: { baseUrl: '../src' } });
write(path.join(root, 'tsconfig.json'), { extends: './config/tsconfig.base.json' });

const aliases = loadProjectAliases(root);
expect(aliases?.declaresBaseUrl).toBe(true);
expect(aliasAndBaseUrlCandidates('utils/format', aliases!, root)).toEqual(['src/utils/format']);
});

it('tries the most specific paths pattern before baseUrl', () => {
write(path.join(root, 'tsconfig.json'), {
compilerOptions: { baseUrl: '.', paths: { 'shared/*': ['libs/shared/src/*'] } },
});

const aliases = loadProjectAliases(root);
expect(aliasAndBaseUrlCandidates('shared/date', aliases!, root)).toEqual(['libs/shared/src/date', 'shared/date']);
expect(aliasAndBaseUrlCandidates('components/Button', aliases!, root)).toEqual(['components/Button']);
});

it('reads a catch-all "*" pattern without a baseUrl, but nothing else from the config directory', () => {
// mantis-free-react-admin-template's vite/jsconfig.json: `paths` alone,
// anchored at the config's own directory (TypeScript 4.1+).
write(path.join(root, 'jsconfig.json'), { compilerOptions: { paths: { '*': ['./src/*'] } } });

const aliases = loadProjectAliases(root);
expect(aliases?.declaresBaseUrl).toBe(false);
expect(aliasAndBaseUrlCandidates('pages/auth/Register', aliases!, root)).toEqual(['src/pages/auth/Register']);

write(path.join(root, 'jsconfig.json'), { compilerOptions: { paths: { '@/*': ['./src/*'] } } });
expect(aliasAndBaseUrlCandidates('pages/auth/Register', loadProjectAliases(root)!, root)).toEqual([]);
});

it('drops a candidate that leaves the project', () => {
write(path.join(root, 'jsconfig.json'), { compilerOptions: { baseUrl: 'src' } });

expect(aliasAndBaseUrlCandidates('../../outside/x', loadProjectAliases(root)!, root)).toEqual([]);
});
});
Loading