Skip to content

Commit

Permalink
Fix loading of the Yarn PnP API (#1151)
Browse files Browse the repository at this point in the history
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"
/>
  • Loading branch information
thecrypticace authored Jan 29, 2025
1 parent cf9cf2e commit 1ea192e
Showing 6 changed files with 88 additions and 30 deletions.
25 changes: 18 additions & 7 deletions packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
@@ -442,16 +442,24 @@ export async function createProjectService(
let applyComplexClasses: any

try {
let tailwindcssPath = await resolver.resolveJsId('tailwindcss', configDir)
let tailwindcssPkgPath = await resolver.resolveJsId('tailwindcss/package.json', configDir)
let tailwindcssPkgPath = await resolver.resolveCjsId('tailwindcss/package.json', configDir)
let tailwindDir = path.dirname(tailwindcssPkgPath)
tailwindcssVersion = require(tailwindcssPkgPath).version

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

tailwindcssPath = pathToFileURL(tailwindcssPath).href
tailwindcss = await import(tailwindcssPath)
// Loading via `await import(…)` with the Yarn PnP API is not possible
if (await resolver.hasPnP()) {
let tailwindcssPath = await resolver.resolveCjsId('tailwindcss', configDir)

tailwindcss = require(tailwindcssPath)
} else {
let tailwindcssPath = await resolver.resolveJsId('tailwindcss', configDir)
let tailwindcssURL = pathToFileURL(tailwindcssPath).href

tailwindcss = await import(tailwindcssURL)
}

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

const postcssPath = resolveFrom(tailwindDir, 'postcss')
const postcssPkgPath = resolveFrom(tailwindDir, 'postcss/package.json')
const postcssPath = await resolver.resolveCjsId('postcss', tailwindDir)
const postcssPkgPath = await resolver.resolveCjsId('postcss/package.json', tailwindDir)
const postcssDir = path.dirname(postcssPkgPath)
const postcssSelectorParserPath = resolveFrom(tailwindDir, 'postcss-selector-parser')
const postcssSelectorParserPath = await resolver.resolveCjsId(
'postcss-selector-parser',
tailwindDir,
)

postcssVersion = require(postcssPkgPath).version

49 changes: 33 additions & 16 deletions packages/tailwindcss-language-server/src/resolver/index.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import {
} from 'enhanced-resolve'
import { loadPnPApi, type PnpApi } from './pnp'
import { loadTsConfig, type TSConfigApi } from './tsconfig'
import { normalizeYarnPnPDriveLetter } from '../utils'

export interface ResolverOptions {
/**
@@ -42,15 +43,6 @@ export interface ResolverOptions {
}

export interface Resolver {
/**
* Sets up the PnP API if it is available such that globals like `require`
* have been monkey-patched to use PnP resolution.
*
* This function does nothing if PnP resolution is not enabled or if the PnP
* API is not available.
*/
setupPnP(): Promise<void>

/**
* Resolves a JavaScript module to a file path.
*
@@ -63,6 +55,16 @@ export interface Resolver {
*/
resolveJsId(id: string, base: string): Promise<string>

/**
* Resolves a CJS module to a file path.
*
* Assumes ESM-captable mechanisms are not available.
*
* @param id The module or file to resolve
* @param base The base directory to resolve the module from
*/
resolveCjsId(id: string, base: string): Promise<string>

/**
* Resolves a CSS module to a file path.
*
@@ -97,6 +99,11 @@ export interface Resolver {
*/
child(opts: Partial<ResolverOptions>): Promise<Resolver>

/**
* Whether or not the PnP API is being used by the resolver
*/
hasPnP(): Promise<boolean>

/**
* Refresh information the resolver may have cached
*
@@ -106,17 +113,18 @@ export interface Resolver {
}

export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
let fileSystem = opts.fileSystem ? opts.fileSystem : new CachedInputFileSystem(fs, 4000)

let pnpApi: PnpApi | null = null

// Load PnP API if requested
// This MUST be done before `CachedInputFileSystem` is created
if (typeof opts.pnp === 'object') {
pnpApi = opts.pnp
} else if (opts.pnp) {
pnpApi = await loadPnPApi(opts.root)
}

let fileSystem = opts.fileSystem ? opts.fileSystem : new CachedInputFileSystem(fs, 4000)

let tsconfig: TSConfigApi | null = null

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

// 2. Normalize the drive letters to the case that the PnP API expects
id = normalizeYarnPnPDriveLetter(id)
base = normalizeYarnPnPDriveLetter(base)

return new Promise((resolve, reject) => {
resolver.resolve({}, base, id, {}, (err, res) => {
if (err) {
@@ -202,6 +214,10 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
}
}

async function resolveCjsId(id: string, base: string): Promise<string> {
return (await resolveId(cjsResolver, id, base)) || id
}

async function resolveCssId(id: string, base: string): Promise<string> {
return (await resolveId(cssResolver, id, base)) || id
}
@@ -212,10 +228,6 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
return (await tsconfig?.substituteId(id, base)) ?? id
}

async function setupPnP() {
pnpApi?.setup()
}

async function aliases(base: string) {
if (!tsconfig) return {}

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

async function hasPnP() {
return !!pnpApi
}

return {
setupPnP,
resolveJsId,
resolveCjsId,
resolveCssId,
substituteId,
refresh,
hasPnP,

aliases,

6 changes: 4 additions & 2 deletions packages/tailwindcss-language-server/src/resolver/pnp.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import findUp from 'find-up'
import * as path from 'node:path'
import { pathToFileURL } from '../utils'

export interface PnpApi {
setup(): void
resolveToUnqualified: (arg0: string, arg1: string, arg2: object) => null | string
}

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

let mod = await import(pnpPath)
let pnpUrl = pathToFileURL(pnpPath).href
let mod = await import(pnpUrl)
let api = mod.default
api.setup()
cache.set(root, api)
return api
}
5 changes: 2 additions & 3 deletions packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,6 @@ import normalizePath from 'normalize-path'
import * as path from 'node:path'
import type * as chokidar from 'chokidar'
import picomatch from 'picomatch'
import { resolveFrom } from './util/resolveFrom'
import * as parcel from './watcher/index.js'
import { equal } from '@tailwindcss/language-service/src/util/array'
import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB, TSCONFIG_GLOB } from './lib/constants'
@@ -321,9 +320,9 @@ export class TW {
let twVersion = require('tailwindcss/package.json').version
try {
let v = require(
resolveFrom(
path.dirname(project.projectConfig.configPath),
await resolver.resolveCjsId(
'tailwindcss/package.json',
path.dirname(project.projectConfig.configPath),
),
).version
if (typeof v === 'string') {
32 changes: 30 additions & 2 deletions packages/tailwindcss-language-server/src/utils.ts
Original file line number Diff line number Diff line change
@@ -74,14 +74,42 @@ export function dirContains(dir: string, file: string): boolean {
}

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

/**
* Windows drive letters are case-insensitive and we may get them as either
* lower or upper case. This function normalizes the drive letter to uppercase
* to be consistent with the rest of the codebase.
*/
export function normalizeDriveLetter(filepath: string) {
return filepath.replace(WIN_DRIVE_LETTER, (_, letter) => letter.toUpperCase() + ':')
return filepath
.replace(WIN_DRIVE_LETTER, (_, letter) => `${letter.toUpperCase()}:`)
.replace(POSIX_DRIVE_LETTER, (_, letter) => `/${letter.toUpperCase()}:`)
}

/**
* Windows drive letters are case-insensitive and we may get them as either
* lower or upper case.
*
* Yarn PnP only works when requests have the correct case for the drive letter
* that matches the drive letter of the current working directory.
*
* Even using makeApi with a custom base path doesn't work around this.
*/
export function normalizeYarnPnPDriveLetter(filepath: string) {
let cwdDriveLetter = process.cwd().match(WIN_DRIVE_LETTER)?.[1]

return filepath
.replace(WIN_DRIVE_LETTER, (_, letter) => {
return letter.toUpperCase() === cwdDriveLetter.toUpperCase()
? `${cwdDriveLetter}:`
: `${letter.toUpperCase()}:`
})
.replace(POSIX_DRIVE_LETTER, (_, letter) => {
return letter.toUpperCase() === cwdDriveLetter.toUpperCase()
? `/${cwdDriveLetter}:`
: `/${letter.toUpperCase()}:`
})
}

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

// If `pathToFileURL` failsed on windows it's probably because the path was
// If `pathToFileURL` failed on windows it's probably because the path was
// a windows network share path and there were mixed slashes.
// Fix the path and try again.
filepath = URI.file(filepath).fsPath
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
## Prerelease

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

## 0.14.1

0 comments on commit 1ea192e

Please sign in to comment.