feat(css): resolve bare @import specifiers to node_modules as fallback#301093
feat(css): resolve bare @import specifiers to node_modules as fallback#301093murataslan1 wants to merge 1 commit intomicrosoft:mainfrom
Conversation
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
There was a problem hiding this comment.
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 attemptnode_modules/<specifier>resolution as a fallback. - Wire the post-processing into the CSS language server’s
onDocumentLinkshandler. - 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. |
| /** | ||
| * 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); |
There was a problem hiding this comment.
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.
| // Check if the resolved target actually exists | ||
| let exists = false; | ||
| try { | ||
| await requestService.stat(link.target); | ||
| exists = true; | ||
| } catch { |
There was a problem hiding this comment.
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.
Summary
Fixes #295074 — CSS
@import "some-module/style.css"now resolves tonode_modules/some-module/style.cssas a fallback when the local file doesn't exist.Problem
CSS
@importsupports 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 fromnode_moduleswith a bare specifier: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:
findDocumentLinks2()returns links, check if each link target actually exists on disk.,/,~, or containing://), if the local file doesn't exist, try resolving againstnode_modules/in the workspace rootnode_modules/version exists, use it as the link targetRules:
@import "foo/style.css"→ triesnode_modules/foo/style.cssif 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)Files Changed
server/src/utils/nodeModuleLinks.tsresolveNodeModuleLinks()utility functionserver/src/cssServer.tsonDocumentLinkshandlerserver/src/test/links.test.tsserver/test/linksTestFixtures/node_modules/foo/hello.html