Skip to content

Commit 1ea192e

Browse files
Fix loading of the Yarn PnP API (#1151)
Fixes #1149 This PR does a few things: - Fixes an error loading the Yarn PnP API on Windows - Fixes a case-sensitivity issue on Windows when using Yarn PnP. Yarn's PnP lookups are case-sensitive down to the drive-letter. The VSCode extension host generally operates with lowercase drive letters but filesystem calls don't return that breaking Yarn's resolution. - Fixes an issue loading Tailwind CSS when Yarn PnP is enabled. We now have to use `require(…)` because that's what's hooked at runtime. It does not work with `await import(…)` unfortunately. I plan to investigate this more to see if I can change this back in the future. We really should not ever load the CJS version of v4. There are most certainly some other problems using Yarn PnP with older Tailwind CSS versions and IntelliSense right now but I plan to address these in a followup PR later. Here's the output panel from a project loaded through Yarn PnP on Windows: <img width="1227" alt="Screenshot 2025-01-29 at 11 33 56" src="https://github.com/user-attachments/assets/3944f907-f74d-4b87-be71-4517ae407cc5" />
1 parent cf9cf2e commit 1ea192e

File tree

6 files changed

+88
-30
lines changed

6 files changed

+88
-30
lines changed

packages/tailwindcss-language-server/src/projects.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -442,16 +442,24 @@ export async function createProjectService(
442442
let applyComplexClasses: any
443443

444444
try {
445-
let tailwindcssPath = await resolver.resolveJsId('tailwindcss', configDir)
446-
let tailwindcssPkgPath = await resolver.resolveJsId('tailwindcss/package.json', configDir)
445+
let tailwindcssPkgPath = await resolver.resolveCjsId('tailwindcss/package.json', configDir)
447446
let tailwindDir = path.dirname(tailwindcssPkgPath)
448447
tailwindcssVersion = require(tailwindcssPkgPath).version
449448

450449
let features = supportedFeatures(tailwindcssVersion)
451450
log(`supported features: ${JSON.stringify(features)}`)
452451

453-
tailwindcssPath = pathToFileURL(tailwindcssPath).href
454-
tailwindcss = await import(tailwindcssPath)
452+
// Loading via `await import(…)` with the Yarn PnP API is not possible
453+
if (await resolver.hasPnP()) {
454+
let tailwindcssPath = await resolver.resolveCjsId('tailwindcss', configDir)
455+
456+
tailwindcss = require(tailwindcssPath)
457+
} else {
458+
let tailwindcssPath = await resolver.resolveJsId('tailwindcss', configDir)
459+
let tailwindcssURL = pathToFileURL(tailwindcssPath).href
460+
461+
tailwindcss = await import(tailwindcssURL)
462+
}
455463

456464
if (!features.includes('css-at-theme')) {
457465
tailwindcss = tailwindcss.default ?? tailwindcss
@@ -484,10 +492,13 @@ export async function createProjectService(
484492
return
485493
}
486494

487-
const postcssPath = resolveFrom(tailwindDir, 'postcss')
488-
const postcssPkgPath = resolveFrom(tailwindDir, 'postcss/package.json')
495+
const postcssPath = await resolver.resolveCjsId('postcss', tailwindDir)
496+
const postcssPkgPath = await resolver.resolveCjsId('postcss/package.json', tailwindDir)
489497
const postcssDir = path.dirname(postcssPkgPath)
490-
const postcssSelectorParserPath = resolveFrom(tailwindDir, 'postcss-selector-parser')
498+
const postcssSelectorParserPath = await resolver.resolveCjsId(
499+
'postcss-selector-parser',
500+
tailwindDir,
501+
)
491502

492503
postcssVersion = require(postcssPkgPath).version
493504

packages/tailwindcss-language-server/src/resolver/index.ts

+33-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from 'enhanced-resolve'
99
import { loadPnPApi, type PnpApi } from './pnp'
1010
import { loadTsConfig, type TSConfigApi } from './tsconfig'
11+
import { normalizeYarnPnPDriveLetter } from '../utils'
1112

1213
export interface ResolverOptions {
1314
/**
@@ -42,15 +43,6 @@ export interface ResolverOptions {
4243
}
4344

4445
export interface Resolver {
45-
/**
46-
* Sets up the PnP API if it is available such that globals like `require`
47-
* have been monkey-patched to use PnP resolution.
48-
*
49-
* This function does nothing if PnP resolution is not enabled or if the PnP
50-
* API is not available.
51-
*/
52-
setupPnP(): Promise<void>
53-
5446
/**
5547
* Resolves a JavaScript module to a file path.
5648
*
@@ -63,6 +55,16 @@ export interface Resolver {
6355
*/
6456
resolveJsId(id: string, base: string): Promise<string>
6557

58+
/**
59+
* Resolves a CJS module to a file path.
60+
*
61+
* Assumes ESM-captable mechanisms are not available.
62+
*
63+
* @param id The module or file to resolve
64+
* @param base The base directory to resolve the module from
65+
*/
66+
resolveCjsId(id: string, base: string): Promise<string>
67+
6668
/**
6769
* Resolves a CSS module to a file path.
6870
*
@@ -97,6 +99,11 @@ export interface Resolver {
9799
*/
98100
child(opts: Partial<ResolverOptions>): Promise<Resolver>
99101

102+
/**
103+
* Whether or not the PnP API is being used by the resolver
104+
*/
105+
hasPnP(): Promise<boolean>
106+
100107
/**
101108
* Refresh information the resolver may have cached
102109
*
@@ -106,17 +113,18 @@ export interface Resolver {
106113
}
107114

108115
export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
109-
let fileSystem = opts.fileSystem ? opts.fileSystem : new CachedInputFileSystem(fs, 4000)
110-
111116
let pnpApi: PnpApi | null = null
112117

113118
// Load PnP API if requested
119+
// This MUST be done before `CachedInputFileSystem` is created
114120
if (typeof opts.pnp === 'object') {
115121
pnpApi = opts.pnp
116122
} else if (opts.pnp) {
117123
pnpApi = await loadPnPApi(opts.root)
118124
}
119125

126+
let fileSystem = opts.fileSystem ? opts.fileSystem : new CachedInputFileSystem(fs, 4000)
127+
120128
let tsconfig: TSConfigApi | null = null
121129

122130
// Load TSConfig path mappings
@@ -183,6 +191,10 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
183191
if (match) id = match
184192
}
185193

194+
// 2. Normalize the drive letters to the case that the PnP API expects
195+
id = normalizeYarnPnPDriveLetter(id)
196+
base = normalizeYarnPnPDriveLetter(base)
197+
186198
return new Promise((resolve, reject) => {
187199
resolver.resolve({}, base, id, {}, (err, res) => {
188200
if (err) {
@@ -202,6 +214,10 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
202214
}
203215
}
204216

217+
async function resolveCjsId(id: string, base: string): Promise<string> {
218+
return (await resolveId(cjsResolver, id, base)) || id
219+
}
220+
205221
async function resolveCssId(id: string, base: string): Promise<string> {
206222
return (await resolveId(cssResolver, id, base)) || id
207223
}
@@ -212,10 +228,6 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
212228
return (await tsconfig?.substituteId(id, base)) ?? id
213229
}
214230

215-
async function setupPnP() {
216-
pnpApi?.setup()
217-
}
218-
219231
async function aliases(base: string) {
220232
if (!tsconfig) return {}
221233

@@ -226,12 +238,17 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
226238
await tsconfig?.refresh()
227239
}
228240

241+
async function hasPnP() {
242+
return !!pnpApi
243+
}
244+
229245
return {
230-
setupPnP,
231246
resolveJsId,
247+
resolveCjsId,
232248
resolveCssId,
233249
substituteId,
234250
refresh,
251+
hasPnP,
235252

236253
aliases,
237254

packages/tailwindcss-language-server/src/resolver/pnp.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import findUp from 'find-up'
22
import * as path from 'node:path'
3+
import { pathToFileURL } from '../utils'
34

45
export interface PnpApi {
5-
setup(): void
66
resolveToUnqualified: (arg0: string, arg1: string, arg2: object) => null | string
77
}
88

@@ -25,8 +25,10 @@ export async function loadPnPApi(root: string): Promise<PnpApi | null> {
2525
return null
2626
}
2727

28-
let mod = await import(pnpPath)
28+
let pnpUrl = pathToFileURL(pnpPath).href
29+
let mod = await import(pnpUrl)
2930
let api = mod.default
31+
api.setup()
3032
cache.set(root, api)
3133
return api
3234
}

packages/tailwindcss-language-server/src/tw.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import normalizePath from 'normalize-path'
3636
import * as path from 'node:path'
3737
import type * as chokidar from 'chokidar'
3838
import picomatch from 'picomatch'
39-
import { resolveFrom } from './util/resolveFrom'
4039
import * as parcel from './watcher/index.js'
4140
import { equal } from '@tailwindcss/language-service/src/util/array'
4241
import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB, TSCONFIG_GLOB } from './lib/constants'
@@ -321,9 +320,9 @@ export class TW {
321320
let twVersion = require('tailwindcss/package.json').version
322321
try {
323322
let v = require(
324-
resolveFrom(
325-
path.dirname(project.projectConfig.configPath),
323+
await resolver.resolveCjsId(
326324
'tailwindcss/package.json',
325+
path.dirname(project.projectConfig.configPath),
327326
),
328327
).version
329328
if (typeof v === 'string') {

packages/tailwindcss-language-server/src/utils.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,42 @@ export function dirContains(dir: string, file: string): boolean {
7474
}
7575

7676
const WIN_DRIVE_LETTER = /^([a-zA-Z]):/
77+
const POSIX_DRIVE_LETTER = /^\/([a-zA-Z]):/
7778

7879
/**
7980
* Windows drive letters are case-insensitive and we may get them as either
8081
* lower or upper case. This function normalizes the drive letter to uppercase
8182
* to be consistent with the rest of the codebase.
8283
*/
8384
export function normalizeDriveLetter(filepath: string) {
84-
return filepath.replace(WIN_DRIVE_LETTER, (_, letter) => letter.toUpperCase() + ':')
85+
return filepath
86+
.replace(WIN_DRIVE_LETTER, (_, letter) => `${letter.toUpperCase()}:`)
87+
.replace(POSIX_DRIVE_LETTER, (_, letter) => `/${letter.toUpperCase()}:`)
88+
}
89+
90+
/**
91+
* Windows drive letters are case-insensitive and we may get them as either
92+
* lower or upper case.
93+
*
94+
* Yarn PnP only works when requests have the correct case for the drive letter
95+
* that matches the drive letter of the current working directory.
96+
*
97+
* Even using makeApi with a custom base path doesn't work around this.
98+
*/
99+
export function normalizeYarnPnPDriveLetter(filepath: string) {
100+
let cwdDriveLetter = process.cwd().match(WIN_DRIVE_LETTER)?.[1]
101+
102+
return filepath
103+
.replace(WIN_DRIVE_LETTER, (_, letter) => {
104+
return letter.toUpperCase() === cwdDriveLetter.toUpperCase()
105+
? `${cwdDriveLetter}:`
106+
: `${letter.toUpperCase()}:`
107+
})
108+
.replace(POSIX_DRIVE_LETTER, (_, letter) => {
109+
return letter.toUpperCase() === cwdDriveLetter.toUpperCase()
110+
? `/${cwdDriveLetter}:`
111+
: `/${letter.toUpperCase()}:`
112+
})
85113
}
86114

87115
export function changeAffectsFile(change: string, files: Iterable<string>): boolean {
@@ -115,7 +143,7 @@ export function pathToFileURL(filepath: string) {
115143
} catch (err) {
116144
if (process.platform !== 'win32') throw err
117145

118-
// If `pathToFileURL` failsed on windows it's probably because the path was
146+
// If `pathToFileURL` failed on windows it's probably because the path was
119147
// a windows network share path and there were mixed slashes.
120148
// Fix the path and try again.
121149
filepath = URI.file(filepath).fsPath

packages/vscode-tailwindcss/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Prerelease
44

55
- Don't suggest `--font-size-*` theme keys in v4.0 ([#1150](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1150))
6+
- Fix detection of Tailwind CSS version when using Yarn PnP ([#1151](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1151))
67

78
## 0.14.1
89

0 commit comments

Comments
 (0)