From 1ea192e6c3bae955a2c9efa6ba992594aad6698b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 29 Jan 2025 11:52:26 -0500 Subject: [PATCH] Fix loading of the Yarn PnP API (#1151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Screenshot 2025-01-29 at 11 33 56 --- .../src/projects.ts | 25 +++++++--- .../src/resolver/index.ts | 49 +++++++++++++------ .../src/resolver/pnp.ts | 6 ++- .../tailwindcss-language-server/src/tw.ts | 5 +- .../tailwindcss-language-server/src/utils.ts | 32 +++++++++++- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 6 files changed, 88 insertions(+), 30 deletions(-) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 426882a7..fb4b901d 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -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 diff --git a/packages/tailwindcss-language-server/src/resolver/index.ts b/packages/tailwindcss-language-server/src/resolver/index.ts index b5c6db94..cc894552 100644 --- a/packages/tailwindcss-language-server/src/resolver/index.ts +++ b/packages/tailwindcss-language-server/src/resolver/index.ts @@ -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 - /** * Resolves a JavaScript module to a file path. * @@ -63,6 +55,16 @@ export interface Resolver { */ resolveJsId(id: string, base: string): Promise + /** + * 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 + /** * Resolves a CSS module to a file path. * @@ -97,6 +99,11 @@ export interface Resolver { */ child(opts: Partial): Promise + /** + * Whether or not the PnP API is being used by the resolver + */ + hasPnP(): Promise + /** * Refresh information the resolver may have cached * @@ -106,17 +113,18 @@ export interface Resolver { } export async function createResolver(opts: ResolverOptions): Promise { - 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 { 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 { } } + async function resolveCjsId(id: string, base: string): Promise { + return (await resolveId(cjsResolver, id, base)) || id + } + async function resolveCssId(id: string, base: string): Promise { return (await resolveId(cssResolver, id, base)) || id } @@ -212,10 +228,6 @@ export async function createResolver(opts: ResolverOptions): Promise { 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 { await tsconfig?.refresh() } + async function hasPnP() { + return !!pnpApi + } + return { - setupPnP, resolveJsId, + resolveCjsId, resolveCssId, substituteId, refresh, + hasPnP, aliases, diff --git a/packages/tailwindcss-language-server/src/resolver/pnp.ts b/packages/tailwindcss-language-server/src/resolver/pnp.ts index 543db0fa..fd7c290d 100644 --- a/packages/tailwindcss-language-server/src/resolver/pnp.ts +++ b/packages/tailwindcss-language-server/src/resolver/pnp.ts @@ -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 { 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 } diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index e5d6522f..587a5000 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -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') { diff --git a/packages/tailwindcss-language-server/src/utils.ts b/packages/tailwindcss-language-server/src/utils.ts index 8e1f6303..e679d213 100644 --- a/packages/tailwindcss-language-server/src/utils.ts +++ b/packages/tailwindcss-language-server/src/utils.ts @@ -74,6 +74,7 @@ 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 @@ -81,7 +82,34 @@ const WIN_DRIVE_LETTER = /^([a-zA-Z]):/ * 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): 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 diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index b0425ba2..10f4404b 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -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