From ee923ac4e4bcc6b32ee553e408c8f7361c330463 Mon Sep 17 00:00:00 2001 From: V Govindarajan Date: Fri, 27 Mar 2026 20:50:06 -0700 Subject: [PATCH] css: resolve bare module specifiers to node_modules in @import Add node_modules resolution for bare module specifiers in CSS @import statements. When a path like "some-module/style.css" is used (without a leading "./", "../", "/", or "~"), resolve it against node_modules in the workspace root, similar to how bundlers like Vite resolve imports. Previously, only relative paths and tilde-prefixed paths (~module) were resolved. This meant Ctrl+clicking @import "some-module/style.css" would fail to navigate to the file even though it is valid CSS when using a bundler. The resolution uses a heuristic: if the import path doesn't match known path patterns (relative, absolute, tilde, or protocol), it is treated as a bare module specifier and resolved against node_modules. Fixes #295074 Signed-off-by: V Govindarajan --- .../server/src/test/links.test.ts | 20 +++++++++++++++++++ .../server/src/utils/documentContext.ts | 14 +++++++++++++ 2 files changed, 34 insertions(+) diff --git a/extensions/css-language-features/server/src/test/links.test.ts b/extensions/css-language-features/server/src/test/links.test.ts index f6b1a349c7062..65cedc0dba4c3 100644 --- a/extensions/css-language-features/server/src/test/links.test.ts +++ b/extensions/css-language-features/server/src/test/links.test.ts @@ -97,4 +97,24 @@ suite('Links', () => { [{ offset: 29, value: '"~foo/hello.html"', target: getTestResource('node_modules/foo/hello.html') }], testUri, folders ); }); + + test('bare module specifier resolving without tilde', async function () { + + const testUri = getTestResource('about.css'); + const folders = [{ name: 'x', uri: getTestResource('') }]; + + await assertLinks('@import "foo/hello.html|"', + [{ offset: 9, value: '"foo/hello.html"', target: getTestResource('node_modules/foo/hello.html') }], testUri, folders + ); + }); + + test('bare module specifier resolving from subfolder', async function () { + + const testUri = getTestResource('subdir/about.css'); + const folders = [{ name: 'x', uri: getTestResource('') }]; + + await assertLinks('@import "foo/hello.html|"', + [{ offset: 9, value: '"foo/hello.html"', target: getTestResource('node_modules/foo/hello.html') }], testUri, folders + ); + }); }); diff --git a/extensions/css-language-features/server/src/utils/documentContext.ts b/extensions/css-language-features/server/src/utils/documentContext.ts index c9f46fb75789b..6dc42dd57fcce 100644 --- a/extensions/css-language-features/server/src/utils/documentContext.ts +++ b/extensions/css-language-features/server/src/utils/documentContext.ts @@ -8,6 +8,11 @@ import { endsWith, startsWith } from '../utils/strings'; import { WorkspaceFolder } from 'vscode-languageserver'; import { Utils, URI } from 'vscode-uri'; +function isBareModuleSpecifier(ref: string): boolean { + // A bare module specifier doesn't start with '.', '..', '/', '~', or a protocol + return !/^(\.\.?\/|\/|~|[a-z][a-z0-9+\-.]*:)/i.test(ref); +} + export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { function getRootFolder(): string | undefined { for (const folder of workspaceFolders) { @@ -30,6 +35,15 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp return folderUri + ref.substring(1); } } + // For bare module specifiers (e.g., "some-module/style.css"), + // resolve against node_modules in the workspace root as a + // fallback, similar to how bundlers like Vite resolve imports. + if (isBareModuleSpecifier(ref)) { + const folderUri = getRootFolder(); + if (folderUri) { + return Utils.resolvePath(URI.parse(folderUri), 'node_modules', ref).toString(true); + } + } const baseUri = URI.parse(base); const baseUriDir = baseUri.path.endsWith('/') ? baseUri : Utils.dirname(baseUri); return Utils.resolvePath(baseUriDir, ref).toString(true);