Skip to content

Fix loading of the Yarn PnP API #1151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
49 changes: 33 additions & 16 deletions packages/tailwindcss-language-server/src/resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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
*
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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 {}

Expand All @@ -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,

Expand Down
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
}

Expand All @@ -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
}
Expand Down
5 changes: 2 additions & 3 deletions packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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') {
Expand Down
32 changes: 30 additions & 2 deletions packages/tailwindcss-language-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down