Skip to content

feat(css): resolve bare @import specifiers to node_modules as fallback#301093

Open
murataslan1 wants to merge 1 commit intomicrosoft:mainfrom
murataslan1:feat/css-import-node-modules
Open

feat(css): resolve bare @import specifiers to node_modules as fallback#301093
murataslan1 wants to merge 1 commit intomicrosoft:mainfrom
murataslan1:feat/css-import-node-modules

Conversation

@murataslan1
Copy link
Contributor

Summary

Fixes #295074 — CSS @import "some-module/style.css" now resolves to node_modules/some-module/style.css as a fallback when the local file doesn't exist.

Problem

CSS @import supports relative paths without leading ./, and VS Code resolves them correctly as relative paths. However, when using a bundler like Vite or Webpack, it's common to import from node_modules with a bare specifier:

@import "some-module/style.css";

Currently, Ctrl+Click on such imports doesn't work because VS Code only resolves the path relative to the current file directory. The ~ prefix workaround (@import "~some-module/style.css") works but is non-standard and not used by most modern bundlers.

Solution

Added a post-processing step in the CSS language server's document link handler:

  1. After findDocumentLinks2() returns links, check if each link target actually exists on disk
  2. For bare specifiers (not starting with ., /, ~, or containing ://), if the local file doesn't exist, try resolving against node_modules/ in the workspace root
  3. If the node_modules/ version exists, use it as the link target

Rules:

  • @import "foo/style.css" → tries node_modules/foo/style.css if local doesn't exist
  • @import "./foo.css" → keeps relative resolution (no fallback)
  • @import "/foo.css" → keeps absolute resolution (no fallback)
  • @import "~foo/style.css" → keeps existing tilde behavior (no fallback)
  • ✅ Local file always takes priority over node_modules

Files Changed

File Change
server/src/utils/nodeModuleLinks.ts New: resolveNodeModuleLinks() utility function
server/src/cssServer.ts Use node_modules fallback in onDocumentLinks handler
server/src/test/links.test.ts 3 test cases: bare module fallback, local priority, relative skip
server/test/linksTestFixtures/node_modules/foo/hello.html Test fixture file

When a CSS @import path is a bare specifier (e.g. 'some-module/style.css')
and the local file doesn't exist, try resolving against node_modules/ as
a fallback. This supports bundler-style imports (Vite, Webpack, etc.)
without requiring the ~ prefix.

- Add resolveNodeModuleLinks() utility that post-processes document links
- Check if resolved path exists via requestService.stat()
- Fall back to workspace_root/node_modules/<path> for bare specifiers
- Skip relative (./) absolute (/) tilde (~) and URL paths
- Add 3 test cases: bare module fallback, local file priority, relative skip

Fixes microsoft#295074
Copilot AI review requested due to automatic review settings March 12, 2026 12:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a node_modules fallback for CSS document links so that bare @import "pkg/path.css"-style specifiers can be Ctrl+Clicked and resolved when the relative file doesn’t exist, aligning VS Code navigation with common bundler behavior.

Changes:

  • Introduce resolveNodeModuleLinks() to post-process CSS document links and attempt node_modules/<specifier> resolution as a fallback.
  • Wire the post-processing into the CSS language server’s onDocumentLinks handler.
  • Add test coverage and fixtures for bare specifier fallback, local-priority behavior, and “no fallback for relative” behavior.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

File Description
extensions/css-language-features/server/src/utils/nodeModuleLinks.ts New utility that checks link existence and optionally rewrites targets to node_modules/… for bare specifiers.
extensions/css-language-features/server/src/cssServer.ts Applies the new link post-processing in the document links handler.
extensions/css-language-features/server/src/test/links.test.ts Adds unit tests for the new fallback behavior.
extensions/css-language-features/server/test/linksTestFixtures/node_modules/foo/hello.html Adds fixture file used by the new tests.

Comment on lines +11 to +35
/**
* For CSS @import paths that look like bare module specifiers (e.g. "some-module/style.css"),
* try resolving against node_modules/ as a fallback when the local file doesn't exist.
* This supports bundler-style imports (Vite, Webpack, etc.) without requiring the ~ prefix.
*/
export async function resolveNodeModuleLinks(
links: DocumentLink[],
document: TextDocument,
workspaceFolders: WorkspaceFolder[],
requestService: RequestService
): Promise<DocumentLink[]> {
const resolvedLinks: DocumentLink[] = [];

for (const link of links) {
if (!link.target) {
resolvedLinks.push(link);
continue;
}

// Extract the original reference text from the document (without quotes)
const refText = document.getText(link.range).replace(/['"]/g, '');

// Only apply fallback for bare specifiers — skip relative, absolute, tilde, URLs
if (!refText || refText.startsWith('.') || refText.startsWith('/') || refText.startsWith('~') || refText.includes('://')) {
resolvedLinks.push(link);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this utility is for CSS @import paths, but the implementation doesn't verify that a DocumentLink came from an @import statement (it will run for url(...) links too, since it only checks refText). Either tighten the logic to only apply to @import links (e.g. inspect surrounding text near link.range), or update the comment/PR intent so callers understand the broader behavior change.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +44
// Check if the resolved target actually exists
let exists = false;
try {
await requestService.stat(link.target);
exists = true;
} catch {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestService.stat() does not consistently throw on missing files in this codebase (e.g. getNodeFSRequestService().stat resolves with type: FileType.Unknown on ENOENT). Using try/catch here will treat missing files as existing, preventing the node_modules fallback (and may also incorrectly accept missing nodeModuleTarget). Consider checking the returned FileStat.type (or otherwise explicitly treating Unknown as non-existent) in both stat calls instead of relying on exceptions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CSS] Support click links in @import '<module>' to node_modules

3 participants