From a342230ac1eebb3a886e80d39a019f69fd3054de Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 14 Dec 2024 15:57:02 -0500 Subject: [PATCH 01/13] Bump esbuild --- package.json | 2 +- packages/tailwindcss-language-server/package.json | 2 +- packages/tailwindcss-language-service/package.json | 2 +- packages/vscode-tailwindcss/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 218a4fb2..deb5fb5e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@npmcli/package-json": "^5.0.0", "@types/culori": "^2.1.0", "culori": "^4.0.1", - "esbuild": "^0.20.2", + "esbuild": "^0.24.0", "minimist": "^1.2.8", "prettier": "^3.2.5", "semver": "^7.5.4" diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 8faf0430..6ae216fe 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -63,7 +63,7 @@ "dlv": "1.1.3", "dset": "3.1.2", "enhanced-resolve": "^5.16.1", - "esbuild": "^0.20.2", + "esbuild": "^0.24.0", "fast-glob": "3.2.4", "find-up": "5.0.0", "jiti": "^2.3.3", diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index d92a10c7..d9893dc0 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -44,7 +44,7 @@ "@types/line-column": "^1.0.2", "@types/node": "^18.19.33", "@types/stringify-object": "^4.0.5", - "esbuild": "^0.20.2", + "esbuild": "^0.24.0", "esbuild-node-externals": "^1.9.0", "minimist": "^1.2.8", "tslib": "2.2.0", diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 53e24ee2..57bbf3da 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -358,7 +358,7 @@ "braces": "3.0.3", "color-name": "1.1.4", "concurrently": "7.0.0", - "esbuild": "^0.20.2", + "esbuild": "^0.24.0", "minimist": "^1.2.8", "move-file-cli": "3.0.0", "normalize-path": "3.0.0", From 4d74b74c95b1d05215b133c415b2b41bd9245623 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sun, 15 Dec 2024 15:37:25 -0500 Subject: [PATCH 02/13] Cleanup --- .../src/css/fix-relative-paths.ts | 2 +- .../src/lib/hook.ts | 2 +- .../tailwindcss-language-server/src/oxide.ts | 2 +- .../src/projects.ts | 26 ++++++++++++------- .../tailwindcss-language-server/src/tw.ts | 2 +- .../src/util/getModuleDependencies.ts | 4 +-- .../src/util/isExcluded.ts | 2 +- .../src/util/v4/design-system.ts | 2 +- .../tailwindcss-language-server/src/utils.ts | 2 +- .../src/documentLinksProvider.ts | 12 ++++----- 10 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/tailwindcss-language-server/src/css/fix-relative-paths.ts b/packages/tailwindcss-language-server/src/css/fix-relative-paths.ts index 46d5d169..df9d38e1 100644 --- a/packages/tailwindcss-language-server/src/css/fix-relative-paths.ts +++ b/packages/tailwindcss-language-server/src/css/fix-relative-paths.ts @@ -1,4 +1,4 @@ -import path from 'node:path' +import * as path from 'node:path' import type { AtRule, Plugin } from 'postcss' import { normalizePath } from '../utils' diff --git a/packages/tailwindcss-language-server/src/lib/hook.ts b/packages/tailwindcss-language-server/src/lib/hook.ts index 9f7a3f96..8d994648 100644 --- a/packages/tailwindcss-language-server/src/lib/hook.ts +++ b/packages/tailwindcss-language-server/src/lib/hook.ts @@ -1,7 +1,7 @@ /** * Adapted from: https://github.com/elastic/require-in-the-middle */ -import Module from 'module' +import Module from 'node:module' import plugins from './plugins' let bundledModules = { diff --git a/packages/tailwindcss-language-server/src/oxide.ts b/packages/tailwindcss-language-server/src/oxide.ts index 7e10da7a..bb8700ff 100644 --- a/packages/tailwindcss-language-server/src/oxide.ts +++ b/packages/tailwindcss-language-server/src/oxide.ts @@ -1,4 +1,4 @@ -import { lte } from 'tailwindcss-language-service/src/util/semver' +import { lte } from '@tailwindcss/language-service/src/util/semver' // This covers the Oxide API from v4.0.0-alpha.1 to v4.0.0-alpha.18 declare namespace OxideV1 { diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 2da5a4c7..5dfb19f2 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -20,8 +20,8 @@ import { FileChangeType } from 'vscode-languageserver/node' import type { TextDocument } from 'vscode-languageserver-textdocument' import { URI } from 'vscode-uri' import { showError, SilentError } from './util/error' -import * as path from 'path' -import * as fs from 'fs' +import * as path from 'node:path' +import * as fs from 'node:fs' import findUp from 'find-up' import picomatch from 'picomatch' import { resolveFrom, setPnpApi } from './util/resolveFrom' @@ -53,7 +53,7 @@ import { getDocumentColors } from '@tailwindcss/language-service/src/documentCol import { getDocumentLinks } from '@tailwindcss/language-service/src/documentLinksProvider' import { debounce } from 'debounce' import { getModuleDependencies } from './util/getModuleDependencies' -import assert from 'assert' +import assert from 'node:assert' // import postcssLoadConfig from 'postcss-load-config' import { bigSign } from '@tailwindcss/language-service/src/util/jit' import { getColor } from '@tailwindcss/language-service/src/util/color' @@ -107,7 +107,7 @@ export interface ProjectService { onDocumentColor(params: DocumentColorParams): Promise onColorPresentation(params: ColorPresentationParams): Promise onCodeAction(params: CodeActionParams): Promise - onDocumentLinks(params: DocumentLinkParams): DocumentLink[] + onDocumentLinks(params: DocumentLinkParams): Promise sortClassLists(classLists: string[]): string[] dependencies(): Iterable @@ -228,7 +228,9 @@ export async function createProjectService( }, async readDirectory(document, directory) { try { - directory = path.resolve(path.dirname(getFileFsPath(document.uri)), directory) + let baseDir = path.dirname(getFileFsPath(document.uri)) + directory = path.resolve(baseDir, directory) + let dirents = await fs.promises.readdir(directory, { withFileTypes: true }) let result: Array<[string, { isDirectory: boolean }] | null> = await Promise.all( @@ -1127,13 +1129,19 @@ export async function createProjectService( return doCodeActions(state, params, document) }, null) }, - onDocumentLinks(params: DocumentLinkParams): DocumentLink[] { + onDocumentLinks(params: DocumentLinkParams): Promise { if (!state.enabled) return null let document = documentService.getDocument(params.textDocument.uri) if (!document) return null - return getDocumentLinks(state, document, (linkPath) => - URI.file(path.resolve(path.dirname(URI.parse(document.uri).fsPath), linkPath)).toString(), - ) + + let documentPath = URI.parse(document.uri).fsPath + let baseDir = path.dirname(documentPath) + + async function resolveTarget(linkPath: string) { + return URI.file(path.resolve(baseDir, linkPath)).toString() + } + + return getDocumentLinks(state, document, resolveTarget) }, provideDiagnostics: debounce( (document: TextDocument) => { diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index aabdc3cf..e87bf347 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -33,7 +33,7 @@ import { } from 'vscode-languageserver/node' import { URI } from 'vscode-uri' import normalizePath from 'normalize-path' -import * as path from 'path' +import * as path from 'node:path' import type * as chokidar from 'chokidar' import picomatch from 'picomatch' import { resolveFrom } from './util/resolveFrom' diff --git a/packages/tailwindcss-language-server/src/util/getModuleDependencies.ts b/packages/tailwindcss-language-server/src/util/getModuleDependencies.ts index aee0119a..84f8872b 100644 --- a/packages/tailwindcss-language-server/src/util/getModuleDependencies.ts +++ b/packages/tailwindcss-language-server/src/util/getModuleDependencies.ts @@ -1,6 +1,6 @@ // https://github.com/tailwindlabs/tailwindcss/blob/bac5ecf0040aa9a788d1b22d706506146ee831ff/src/lib/getModuleDependencies.js -import fs from 'fs' -import path from 'path' +import * as fs from 'node:fs' +import * as path from 'node:path' import { normalizeDriveLetter, normalizePath } from '../utils' let jsExtensions = ['.js', '.cjs', '.mjs'] diff --git a/packages/tailwindcss-language-server/src/util/isExcluded.ts b/packages/tailwindcss-language-server/src/util/isExcluded.ts index bbb6ca58..beb4115a 100644 --- a/packages/tailwindcss-language-server/src/util/isExcluded.ts +++ b/packages/tailwindcss-language-server/src/util/isExcluded.ts @@ -1,5 +1,5 @@ import picomatch from 'picomatch' -import * as path from 'path' +import * as path from 'node:path' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { State } from '@tailwindcss/language-service/src/util/state' import { getFileFsPath } from './uri' diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 8e8ad5be..cc5a23f9 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -6,7 +6,7 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import { resolveCssFrom, resolveCssImports } from '../../css' import { resolveFrom } from '../resolveFrom' -import { pathToFileURL } from 'tailwindcss-language-server/src/utils' +import { pathToFileURL } from '../../utils' import type { Jiti } from 'jiti/lib/types' const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/ diff --git a/packages/tailwindcss-language-server/src/utils.ts b/packages/tailwindcss-language-server/src/utils.ts index 1660ce80..8e1f6303 100644 --- a/packages/tailwindcss-language-server/src/utils.ts +++ b/packages/tailwindcss-language-server/src/utils.ts @@ -1,5 +1,5 @@ import Module from 'node:module' -import path from 'node:path' +import * as path from 'node:path' import { URI } from 'vscode-uri' import normalizePathBase from 'normalize-path' import { pathToFileURL as pathToFileURLBase } from 'node:url' diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index b18a4711..a0fcd1a2 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -11,8 +11,8 @@ const HAS_DRIVE_LETTER = /^[A-Z]:/ export function getDocumentLinks( state: State, document: TextDocument, - resolveTarget: (linkPath: string) => string, -): DocumentLink[] { + resolveTarget: (linkPath: string) => Promise, +): Promise { let patterns = [/@config\s*(?'[^']+'|"[^"]+")/g] if (state.v4) { @@ -27,12 +27,12 @@ export function getDocumentLinks( return getDirectiveLinks(state, document, patterns, resolveTarget) } -function getDirectiveLinks( +async function getDirectiveLinks( state: State, document: TextDocument, patterns: RegExp[], - resolveTarget: (linkPath: string) => string, -): DocumentLink[] { + resolveTarget: (linkPath: string) => Promise, +): Promise { if (!semver.gte(state.version, '3.2.0')) { return [] } @@ -67,7 +67,7 @@ function getDirectiveLinks( } links.push({ - target: resolveTarget(path), + target: await resolveTarget(path), range: absoluteRange(range, block.range), }) } From f802892aee90fac07b2d2196f35221f883e4cd7b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 14 Dec 2024 19:52:28 -0500 Subject: [PATCH 03/13] Print config path alongside tailwind version --- .../tailwindcss-language-server/src/project-locator.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 0c025914..194ac085 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -130,7 +130,12 @@ export class ProjectLocator { private async createProject(config: ConfigEntry): Promise { let tailwind = await this.detectTailwindVersion(config) - console.log(JSON.stringify({ tailwind })) + console.log( + JSON.stringify({ + tailwind, + path: config.path, + }), + ) // A JS/TS config file was loaded from an `@config` directive in a CSS file // This is only relevant for v3 projects so we'll do some feature detection From e1fd5cc6baaab8a72e20127e563c80f37baeeeb3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 5 Dec 2024 16:24:58 -0500 Subject: [PATCH 04/13] Recover from missing imports when reloading the design system --- .../src/util/v4/design-system.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index cc5a23f9..100c2f79 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -136,13 +136,24 @@ export async function loadDesignSystem( }), loadStylesheet: async (id: string, base: string) => { - let resolved = resolveCssFrom(base, id) + // Skip over missing stylesheets (and log an error) so we can do our best + // to compile the design system even when the build might be incomplete. + // TODO: Figure out if we can recover from parsing errors in stylesheets + // we'd want to surface diagnostics when we discover imports that cause + // parsing errors or other logic errors. - dependencies.add(resolved) + try { + let resolved = resolveCssFrom(base, id) + + dependencies.add(resolved) - return { - base: path.dirname(resolved), - content: await fs.readFile(resolved, 'utf-8'), + return { + base: path.dirname(resolved), + content: await fs.readFile(resolved, 'utf-8'), + } + } catch (err) { + console.error(`Unable to load stylesheet: ${id}`, err) + return { base, content: '' } } }, From 664ea21d8be5ad4c279d38a6d4251045ed4e7ed6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 11 Dec 2024 12:24:58 -0500 Subject: [PATCH 05/13] Recover from missing imports in the project locator --- .../src/css/resolve-css-imports.ts | 18 +++++++++++++++++- .../src/project-locator.test.ts | 8 ++++++++ .../src/project-locator.ts | 6 ++++-- .../tests/fixtures/v4/missing-files/app.css | 4 ++++ .../fixtures/v4/missing-files/i-exist.css | 3 +++ .../v4/missing-files/package-lock.json | 17 +++++++++++++++++ .../fixtures/v4/missing-files/package.json | 5 +++++ 7 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/app.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/i-exist.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json diff --git a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts index 788b5eea..555229a9 100644 --- a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts +++ b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts @@ -1,3 +1,4 @@ +import * as fs from 'node:fs/promises' import postcss from 'postcss' import postcssImport from 'postcss-import' import { createResolver } from '../util/resolve' @@ -11,7 +12,22 @@ const resolver = createResolver({ const resolveImports = postcss([ postcssImport({ - resolve: (id, base) => resolveCssFrom(base, id), + resolve(id, base) { + try { + return resolveCssFrom(base, id) + } catch (e) { + // TODO: Need to test this on windows + return `/virtual:missing/${id}` + } + }, + + load(filepath) { + if (filepath.startsWith('/virtual:missing/')) { + return Promise.resolve('') + } + + return fs.readFile(filepath, 'utf-8') + }, }), fixRelativePaths(), ]) diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index e47dd534..63985d07 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -195,3 +195,11 @@ testFixture('v4/custom-source', [ ], }, ]) + +testFixture('v4/missing-files', [ + // + { + config: 'app.css', + content: ['{URL}/package.json'], + }, +]) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 194ac085..d097b266 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -326,8 +326,6 @@ export class ProjectLocator { // we'll verify after config resolution. let configPath = file.configPathInCss() if (configPath) { - // We don't need the content for this file anymore - file.content = null file.configs.push( configs.remember(configPath, () => ({ // A CSS file produced a JS config file @@ -604,6 +602,10 @@ class FileEntry { let result = await resolveCssImports().process(this.content, { from: this.path }) let deps = result.messages.filter((msg) => msg.type === 'dependency') + deps = deps.filter((msg) => { + return !msg.file.startsWith('/virtual:missing/') + }) + // Record entries for each of the dependencies this.deps = deps.map((msg) => new FileEntry('css', normalizePath(msg.file))) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/app.css b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/app.css new file mode 100644 index 00000000..cbbdcb4a --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/app.css @@ -0,0 +1,4 @@ +@import 'tailwindcss'; + +@import './i-do-not-exist.css'; +@import './i-exist.css'; diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/i-exist.css b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/i-exist.css new file mode 100644 index 00000000..a15c877a --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/i-exist.css @@ -0,0 +1,3 @@ +.foo { + color: red; +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json new file mode 100644 index 00000000..0ea4e1a1 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "missing-files", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0-beta.6.tgz", + "integrity": "sha512-eCCuMk3H65w4J/QWkjxfeWoBSKbCD3E6Uj2LA6Xkkl4eMa1MXuwVpIU1RXcLIp+BVsXGEZMP7i7uJig7KxfAXQ==" + } + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json new file mode 100644 index 00000000..960bfb88 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } +} From 245f0b2a0ce2c7bd578c8f2718ad96453102427a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 11 Dec 2024 14:16:06 -0500 Subject: [PATCH 06/13] Refactor module resolution --- .../src/css/resolve-css-imports.ts | 58 +++--- .../src/project-locator.test.ts | 4 +- .../src/project-locator.ts | 47 +++-- .../src/projects.ts | 47 +++-- .../src/resolver/index.ts | 170 ++++++++++++++++++ .../src/resolver/pnp.ts | 49 +++++ .../tailwindcss-language-server/src/tw.ts | 11 +- .../src/util/resolve.ts | 16 -- .../src/util/resolveFrom.ts | 26 ++- .../src/util/v4/design-system.ts | 16 +- 10 files changed, 360 insertions(+), 84 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/resolver/index.ts create mode 100644 packages/tailwindcss-language-server/src/resolver/pnp.ts delete mode 100644 packages/tailwindcss-language-server/src/util/resolve.ts diff --git a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts index 555229a9..31b598c9 100644 --- a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts +++ b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts @@ -1,41 +1,33 @@ import * as fs from 'node:fs/promises' import postcss from 'postcss' import postcssImport from 'postcss-import' -import { createResolver } from '../util/resolve' import { fixRelativePaths } from './fix-relative-paths' +import { Resolver } from '../resolver' -const resolver = createResolver({ - extensions: ['.css'], - mainFields: ['style'], - conditionNames: ['style'], -}) +export function resolveCssImports({ + resolver, +}: { + resolver: Resolver +}) { + return postcss([ + postcssImport({ + async resolve(id, base) { + try { + return await resolver.resolveCssId(id, base) + } catch (e) { + // TODO: Need to test this on windows + return `/virtual:missing/${id}` + } + }, -const resolveImports = postcss([ - postcssImport({ - resolve(id, base) { - try { - return resolveCssFrom(base, id) - } catch (e) { - // TODO: Need to test this on windows - return `/virtual:missing/${id}` - } - }, + load(filepath) { + if (filepath.startsWith('/virtual:missing/')) { + return Promise.resolve('') + } - load(filepath) { - if (filepath.startsWith('/virtual:missing/')) { - return Promise.resolve('') - } - - return fs.readFile(filepath, 'utf-8') - }, - }), - fixRelativePaths(), -]) - -export function resolveCssImports() { - return resolveImports -} - -export function resolveCssFrom(base: string, id: string) { - return resolver.resolveSync({}, base, id) || id + return fs.readFile(filepath, 'utf-8') + }, + }), + fixRelativePaths(), + ]) } diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 63985d07..7261e736 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -3,6 +3,7 @@ import * as path from 'node:path' import { ProjectLocator } from './project-locator' import { URL, fileURLToPath } from 'url' import { Settings } from '@tailwindcss/language-service/src/util/state' +import { createResolver } from './resolver' let settings: Settings = { tailwindCSS: { @@ -17,7 +18,8 @@ function testFixture(fixture: string, details: any[]) { let fixturePath = `${fixtures}/${fixture}` test.concurrent(fixture, async ({ expect }) => { - let locator = new ProjectLocator(fixturePath, settings) + let resolver = await createResolver({ root: fixturePath }) + let locator = new ProjectLocator(fixturePath, settings, resolver) let projects = await locator.search() for (let i = 0; i < Math.max(projects.length, details.length); i++) { diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index d097b266..e12927db 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -7,11 +7,10 @@ import type { Settings } from '@tailwindcss/language-service/src/util/state' import { CONFIG_GLOB, CSS_GLOB } from './lib/constants' import { readCssFile } from './util/css' import { Graph } from './graph' -import type { AtRule, Message } from 'postcss' import { type DocumentSelector, DocumentSelectorPriority } from './projects' import { CacheMap } from './cache-map' import { getPackageRoot } from './util/get-package-root' -import { resolveFrom } from './util/resolveFrom' +import type { Resolver } from './resolver' import { type Feature, supportedFeatures } from '@tailwindcss/language-service/src/features' import { extractSourceDirectives, resolveCssImports } from './css' import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils' @@ -46,6 +45,7 @@ export class ProjectLocator { constructor( private base: string, private settings: Settings, + private resolver: Resolver, ) {} async search(): Promise { @@ -196,7 +196,11 @@ export class ProjectLocator { }) // - Content patterns from config - for await (let selector of contentSelectorsFromConfig(config, tailwind.features)) { + for await (let selector of contentSelectorsFromConfig( + config, + tailwind.features, + this.resolver, + )) { selectors.push(selector) } @@ -343,7 +347,7 @@ export class ProjectLocator { } // Resolve imports in all the CSS files - await Promise.all(imports.map((file) => file.resolveImports())) + await Promise.all(imports.map((file) => file.resolveImports(this.resolver))) // Resolve real paths for all the files in the CSS import graph await Promise.all(imports.map((file) => file.resolveRealpaths())) @@ -421,7 +425,10 @@ export class ProjectLocator { private async detectTailwindVersion(config: ConfigEntry) { try { - let metadataPath = resolveFrom(path.dirname(config.path), 'tailwindcss/package.json') + let metadataPath = await this.resolver.resolveJsId( + 'tailwindcss/package.json', + path.dirname(config.path), + ) let { version } = require(metadataPath) let features = supportedFeatures(version) @@ -448,14 +455,14 @@ export class ProjectLocator { function contentSelectorsFromConfig( entry: ConfigEntry, features: Feature[], - actualConfig?: any, + resolver: Resolver, ): AsyncIterable { if (entry.type === 'css') { - return contentSelectorsFromCssConfig(entry) + return contentSelectorsFromCssConfig(entry, resolver) } if (entry.type === 'js') { - return contentSelectorsFromJsConfig(entry, features, actualConfig) + return contentSelectorsFromJsConfig(entry, features) } } @@ -500,7 +507,10 @@ async function* contentSelectorsFromJsConfig( } } -async function* contentSelectorsFromCssConfig(entry: ConfigEntry): AsyncIterable { +async function* contentSelectorsFromCssConfig( + entry: ConfigEntry, + resolver: Resolver, +): AsyncIterable { let auto = false for (let item of entry.content) { if (item.kind === 'file') { @@ -516,7 +526,12 @@ async function* contentSelectorsFromCssConfig(entry: ConfigEntry): AsyncIterable // other entries should have sources. let sources = entry.entries.flatMap((entry) => entry.sources) - for await (let pattern of detectContentFiles(entry.packageRoot, entry.path, sources)) { + for await (let pattern of detectContentFiles( + entry.packageRoot, + entry.path, + sources, + resolver, + )) { yield { pattern, priority: DocumentSelectorPriority.CONTENT_FILE, @@ -530,11 +545,15 @@ async function* detectContentFiles( base: string, inputFile: string, sources: string[], + resolver: Resolver, ): AsyncIterable { try { - let oxidePath = resolveFrom(path.dirname(base), '@tailwindcss/oxide') + let oxidePath = await resolver.resolveJsId('@tailwindcss/oxide', path.dirname(base)) oxidePath = pathToFileURL(oxidePath).href - let oxidePackageJsonPath = resolveFrom(path.dirname(base), '@tailwindcss/oxide/package.json') + let oxidePackageJsonPath = await resolver.resolveJsId( + '@tailwindcss/oxide/package.json', + path.dirname(base), + ) let oxidePackageJson = JSON.parse(await fs.readFile(oxidePackageJsonPath, 'utf8')) let result = await oxide.scan({ @@ -597,9 +616,9 @@ class FileEntry { } } - async resolveImports() { + async resolveImports(resolver: Resolver) { try { - let result = await resolveCssImports().process(this.content, { from: this.path }) + let result = await resolveCssImports({ resolver }).process(this.content, { from: this.path }) let deps = result.messages.filter((msg) => msg.type === 'dependency') deps = deps.filter((msg) => { diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 5dfb19f2..5d90f0b3 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -35,6 +35,7 @@ import stackTrace from 'stack-trace' import extractClassNames from './lib/extractClassNames' import { klona } from 'klona/full' import { doHover } from '@tailwindcss/language-service/src/hoverProvider' +import { Resolver } from './resolver' import { doComplete, resolveCompletionItem, @@ -80,6 +81,7 @@ import type { ProjectConfig } from './project-locator' import { supportedFeatures } from '@tailwindcss/language-service/src/features' import { loadDesignSystem } from './util/v4' import { readCssFile } from './util/css' +import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4' const colorNames = Object.keys(namedColors) @@ -186,6 +188,7 @@ export async function createProjectService( initialTailwindVersion: string, getConfiguration: (uri?: string) => Promise, userLanguages: Record, + parentResolver?: Resolver, ): Promise { /* Project dependencies require a design system reload */ let dependencies = new Set() @@ -267,6 +270,10 @@ export async function createProjectService( ]) } + let resolver = await parentResolver.child({ + root: projectConfig.folder, + }) + function log(...args: string[]): void { console.log( `[${path.relative(projectConfig.folder, projectConfig.configPath)}] ${args.join(' ')}`, @@ -434,9 +441,9 @@ export async function createProjectService( let applyComplexClasses: any try { - let tailwindcssPath = resolveFrom(configDir, 'tailwindcss') - const tailwindcssPkgPath = resolveFrom(configDir, 'tailwindcss/package.json') - const tailwindDir = path.dirname(tailwindcssPkgPath) + let tailwindcssPath = await resolver.resolveJsId('tailwindcss', configDir) + let tailwindcssPkgPath = await resolver.resolveJsId('tailwindcss/package.json', configDir) + let tailwindDir = path.dirname(tailwindcssPkgPath) tailwindcssVersion = require(tailwindcssPkgPath).version let features = supportedFeatures(tailwindcssVersion) @@ -444,13 +451,16 @@ export async function createProjectService( tailwindcssPath = pathToFileURL(tailwindcssPath).href tailwindcss = await import(tailwindcssPath) - tailwindcss = tailwindcss.default ?? tailwindcss + + if (!features.includes('css-at-theme')) { + tailwindcss = tailwindcss.default ?? tailwindcss + } + log(`Loaded tailwindcss v${tailwindcssVersion}: ${tailwindDir}`) if (features.includes('css-at-theme')) { state.configPath = configPath state.version = tailwindcssVersion - // TODO: Handle backwards compat stuff here too state.isCssConfig = true state.v4 = true state.jit = true @@ -758,6 +768,7 @@ export async function createProjectService( try { let css = await readCssFile(state.configPath) let designSystem = await loadDesignSystem( + resolver, state.modules.tailwindcss.module, state.configPath, css, @@ -1025,12 +1036,24 @@ export async function createProjectService( if (!state.v4) return console.log('---- RELOADING DESIGN SYSTEM ----') + console.log(`---- ${state.configPath} ----`) + let css = await readCssFile(state.configPath) - let designSystem = await loadDesignSystem( - state.modules.tailwindcss.module, - state.configPath, - css, - ) + let designSystem: DesignSystem + + let start = process.hrtime.bigint() + + try { + designSystem = await loadDesignSystem( + resolver, + state.modules.tailwindcss.module, + state.configPath, + css, + ) + } catch (err) { + console.error(err) + throw err + } // TODO: This is weird and should be changed // We use Object.create so no global state is mutated until necessary @@ -1066,7 +1089,9 @@ export async function createProjectService( // TODO: Need to verify how well this works across various editors // updateCapabilities() - console.log('---- RELOADED ----') + let elapsed = process.hrtime.bigint() - start + + console.log(`---- RELOADED IN ${(Number(elapsed) / 1e6).toFixed(2)}ms ----`) }, state, diff --git a/packages/tailwindcss-language-server/src/resolver/index.ts b/packages/tailwindcss-language-server/src/resolver/index.ts new file mode 100644 index 00000000..b57436fb --- /dev/null +++ b/packages/tailwindcss-language-server/src/resolver/index.ts @@ -0,0 +1,170 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import { + CachedInputFileSystem, + ResolverFactory, + Resolver as BaseResolver, + FileSystem, +} from 'enhanced-resolve' +import { loadPnPApi, type PnpApi } from './pnp' + +export interface ResolverOptions { + /** + * The root directory for the resolver + */ + root: string + + /** + * Whether or not the resolver should attempt to use PnP resolution. + * + * If `true`, the resolver will attempt to load the PnP API and use it for + * resolution. However, if an API is provided, the resolver will use that API + * instead. + */ + pnp?: boolean | PnpApi + + /** + * A filesystem to use for resolution. If not provided, the resolver will + * create one and use it internally for itself and any child resolvers that + * do not provide their own filesystem. + */ + fileSystem?: FileSystem +} + +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. + * + * Assumes dynamic imports or some other ESM-captable mechanism will be used + * to load the module. Tries to resolve the ESM module first, then falls back + * to the CommonJS module if the ESM module is not found. + * + * @param id The module or file to resolve + * @param base The base directory to resolve the module from + */ + resolveJsId(id: string, base: string): Promise + + /** + * Resolves a CSS module to a file path. + * + * @param id The module or file to resolve + * @param base The base directory to resolve the module from + */ + resolveCssId(id: string, base: string): Promise + + /** + * Create a child resolver with the given options. + * + * Use this to share state between resolvers. For example, if a resolver has + * already loaded the PnP API, you can create a child resolver that reuses + * the same PnP API without needing to load it again. + */ + child(opts: Partial): Promise +} + +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 + if (typeof opts.pnp === 'object') { + pnpApi = opts.pnp + } else if (opts.pnp) { + pnpApi = await loadPnPApi(opts.root) + } + + let esmResolver = ResolverFactory.createResolver({ + fileSystem, + extensions: ['.mjs', '.js'], + mainFields: ['module'], + conditionNames: ['node', 'import'], + pnpApi, + }) + + let cjsResolver = ResolverFactory.createResolver({ + fileSystem, + extensions: ['.cjs', '.js'], + mainFields: ['main'], + conditionNames: ['node', 'require'], + pnpApi, + }) + + let cssResolver = ResolverFactory.createResolver({ + fileSystem, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], + pnpApi, + }) + + async function resolveId( + resolver: BaseResolver, + id: string, + base: string, + ): Promise { + // Windows-specific path tweaks + if (path.sep === '\\') { + // Absolute path on Network Share + if (id.startsWith('\\\\')) return id + + // Absolute path on Network Share (normalized) + if (id.startsWith('//')) return id + + // Relative to Network Share (normalized) + if (base.startsWith('//')) base = `\\\\${base.slice(2)}` + } + + return new Promise((resolve, reject) => { + resolver.resolve({}, base, id, {}, (err, res) => { + if (err) { + reject(err) + } else { + resolve(res) + } + }) + }) + } + + async function resolveJsId(id: string, base: string): Promise { + try { + return (await resolveId(esmResolver, id, base)) || id + } catch { + return (await resolveId(cjsResolver, id, base)) || id + } + } + + async function resolveCssId(id: string, base: string): Promise { + return (await resolveId(cssResolver, id, base)) || id + } + + async function setupPnP() { + pnpApi?.setup() + } + + return { + setupPnP, + resolveJsId, + resolveCssId, + + child(childOpts: Partial) { + return createResolver({ + ...opts, + ...childOpts, + + // Inherit defaults from parent + pnp: childOpts.pnp ?? pnpApi, + fileSystem: childOpts.fileSystem ?? fileSystem, + }) + }, + } +} diff --git a/packages/tailwindcss-language-server/src/resolver/pnp.ts b/packages/tailwindcss-language-server/src/resolver/pnp.ts new file mode 100644 index 00000000..543db0fa --- /dev/null +++ b/packages/tailwindcss-language-server/src/resolver/pnp.ts @@ -0,0 +1,49 @@ +import findUp from 'find-up' +import * as path from 'node:path' + +export interface PnpApi { + setup(): void + resolveToUnqualified: (arg0: string, arg1: string, arg2: object) => null | string +} + +const cache = new Map() + +/** + * Loads the PnP API from the given directory if found. + * We intentionally do not call `setup` to monkey patch global APIs + * TODO: Verify that we can get by without doing this + */ +export async function loadPnPApi(root: string): Promise { + let existing = cache.get(root) + if (existing !== undefined) { + return existing + } + + let pnpPath = await findPnPApi(path.normalize(root)) + if (!pnpPath) { + cache.set(root, null) + return null + } + + let mod = await import(pnpPath) + let api = mod.default + cache.set(root, api) + return api +} + +/** + * Locates the PnP API file for a given directory + */ +async function findPnPApi(root: string): Promise { + let names = ['.pnp.js', '.pnp.cjs'] + + for (let name of names) { + let filepath = path.join(root, name) + + if (await findUp.exists(filepath)) { + return filepath + } + } + + return null +} diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index e87bf347..79d8b6cf 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -47,6 +47,7 @@ import { type SettingsCache, createSettingsCache } from './config' import { readCssFile } from './util/css' import { ProjectLocator, type ProjectConfig } from './project-locator' import type { TailwindCssSettings } from '@tailwindcss/language-service/src/util/state' +import { createResolver, Resolver } from './resolver' const TRIGGER_CHARACTERS = [ // class attributes @@ -243,7 +244,12 @@ export class TW { return } - let locator = new ProjectLocator(base, globalSettings) + let resolver = await createResolver({ + root: base, + pnp: true, + }) + + let locator = new ProjectLocator(base, globalSettings, resolver) if (configs.length > 0) { console.log('Loading Tailwind CSS projects from the workspace settings.') @@ -524,6 +530,7 @@ export class TW { this.watchPatterns, configTailwindVersionMap.get(projectConfig.configPath), userLanguages, + resolver, ), ), ) @@ -654,6 +661,7 @@ export class TW { watchPatterns: (patterns: string[]) => void, tailwindVersion: string, userLanguages: Record, + resolver: Resolver, ): Promise { let key = String(this.projectCounter++) const project = await createProjectService( @@ -678,6 +686,7 @@ export class TW { tailwindVersion, this.settingsCache.get, userLanguages, + resolver, ) this.projects.set(key, project) diff --git a/packages/tailwindcss-language-server/src/util/resolve.ts b/packages/tailwindcss-language-server/src/util/resolve.ts deleted file mode 100644 index 8d6e9504..00000000 --- a/packages/tailwindcss-language-server/src/util/resolve.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as fs from 'fs' -import { - CachedInputFileSystem, - ResolverFactory, - Resolver, - ResolveOptions, -} from 'enhanced-resolve' - -export function createResolver(options: Partial = {}): Resolver { - return ResolverFactory.createResolver({ - fileSystem: new CachedInputFileSystem(fs, 4000), - useSyncFileSystemCalls: true, - conditionNames: ['node', 'require'], - ...options, - }) -} diff --git a/packages/tailwindcss-language-server/src/util/resolveFrom.ts b/packages/tailwindcss-language-server/src/util/resolveFrom.ts index f81cccf7..2b62bbeb 100644 --- a/packages/tailwindcss-language-server/src/util/resolveFrom.ts +++ b/packages/tailwindcss-language-server/src/util/resolveFrom.ts @@ -1,21 +1,41 @@ -import { equal } from '@tailwindcss/language-service/src/util/array' +import * as fs from 'node:fs' import * as path from 'node:path' -import { createResolver } from './resolve' +import { equal } from '@tailwindcss/language-service/src/util/array' +import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve' let pnpApi: any let extensions = Object.keys(require.extensions) function recreateResolver() { - return createResolver({ extensions, pnpApi }) + let fileSystem = new CachedInputFileSystem(fs, 4000) + + return ResolverFactory.createResolver({ + fileSystem, + useSyncFileSystemCalls: true, + conditionNames: ['node', 'require'], + extensions, + pnpApi, + }) } let resolver = recreateResolver() +/** + * @deprecated Use `createResolver()` instead. + */ export function setPnpApi(newPnpApi: any): void { pnpApi = newPnpApi resolver = recreateResolver() } +/** + * Resolve a module id from a given path synchronously. + * + * This is a legacy API and should be avoided in favor of the async version as + * it does not support TypeScript path mapping. + * + * @deprecated Use `createResolver().resolveJsId(…)` instead. + */ export function resolveFrom(from?: string, id?: string): string { // Network share path on Windows if (id.startsWith('\\\\')) return id diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 100c2f79..5631ba43 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -4,8 +4,8 @@ import postcss from 'postcss' import { createJiti } from 'jiti' import * as fs from 'node:fs/promises' import * as path from 'node:path' -import { resolveCssFrom, resolveCssImports } from '../../css' -import { resolveFrom } from '../resolveFrom' +import { resolveCssImports } from '../../css' +import { Resolver } from '../../resolver' import { pathToFileURL } from '../../utils' import type { Jiti } from 'jiti/lib/types' @@ -47,18 +47,20 @@ function createLoader({ dependencies, legacy, filepath, + resolver, onError, }: { dependencies: Set legacy: boolean filepath: string + resolver: Resolver onError: (id: string, error: unknown, resourceType: string) => T }) { let cacheKey = `${+Date.now()}` async function loadFile(id: string, base: string, resourceType: string) { try { - let resolved = resolveFrom(base, id) + let resolved = await resolver.resolveJsId(id, base) dependencies.add(resolved) @@ -85,6 +87,7 @@ function createLoader({ } export async function loadDesignSystem( + resolver: Resolver, tailwindcss: any, filepath: string, css: string, @@ -111,7 +114,7 @@ export async function loadDesignSystem( // Step 2: Use postcss to resolve `@import` rules in the CSS file if (!supportsImports) { - let resolved = await resolveCssImports().process(css, { from: filepath }) + let resolved = await resolveCssImports({ resolver }).process(css, { from: filepath }) css = resolved.css } @@ -124,6 +127,7 @@ export async function loadDesignSystem( dependencies, legacy: false, filepath, + resolver, onError: (id, err, resourceType) => { console.error(`Unable to load ${resourceType}: ${id}`, err) @@ -143,7 +147,7 @@ export async function loadDesignSystem( // parsing errors or other logic errors. try { - let resolved = resolveCssFrom(base, id) + let resolved = await resolver.resolveCssId(id, base) dependencies.add(resolved) @@ -162,6 +166,7 @@ export async function loadDesignSystem( dependencies, legacy: true, filepath, + resolver, onError(id, err) { console.error(`Unable to load plugin: ${id}`, err) @@ -173,6 +178,7 @@ export async function loadDesignSystem( dependencies, legacy: true, filepath, + resolver, onError(id, err) { console.error(`Unable to load config: ${id}`, err) From 440f1c16f1e48253fc46cd3665348cfcf1a3d58f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 14:35:13 -0500 Subject: [PATCH 07/13] Move Jiti initialization to `loadDesignSystem` --- .../src/util/v4/design-system.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 5631ba43..8211bac6 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -24,20 +24,6 @@ export async function isMaybeV4(css: string): Promise { return HAS_V4_THEME.test(css) || HAS_V4_IMPORT.test(css) } -let jiti: Jiti | undefined - -async function importFile(id: string) { - try { - // Load ESM/CJS files through Node/Bun/whatever runtime is being used - return await import(id) - } catch { - jiti ??= createJiti(__filename, { moduleCache: false, fsCache: false }) - - // Transpile using Jiti if we can't load the file directly - return await jiti.import(id) - } -} - /** * Create a loader function that can load plugins and config files relative to * the CSS file that uses them. However, we don't want missing files to prevent @@ -46,12 +32,14 @@ async function importFile(id: string) { function createLoader({ dependencies, legacy, + jiti, filepath, resolver, onError, }: { dependencies: Set legacy: boolean + jiti: Jiti filepath: string resolver: Resolver onError: (id: string, error: unknown, resourceType: string) => T @@ -67,7 +55,7 @@ function createLoader({ let url = pathToFileURL(resolved) url.searchParams.append('t', cacheKey) - return await importFile(url.href).then((m) => m.default ?? m) + return await jiti.import(url.href, { default: true }) } catch (err) { return onError(id, err, resourceType) } @@ -118,6 +106,12 @@ export async function loadDesignSystem( css = resolved.css } + // Create a Jiti instance that can be used to load plugins and config files + let jiti = createJiti(__filename, { + moduleCache: false, + fsCache: false, + }) + // Step 3: Take the resolved CSS and pass it to v4's `loadDesignSystem` let design: DesignSystem = await tailwindcss.__unstable__loadDesignSystem(css, { base: path.dirname(filepath), @@ -126,6 +120,7 @@ export async function loadDesignSystem( loadModule: createLoader({ dependencies, legacy: false, + jiti, filepath, resolver, onError: (id, err, resourceType) => { @@ -165,6 +160,7 @@ export async function loadDesignSystem( loadPlugin: createLoader({ dependencies, legacy: true, + jiti, filepath, resolver, onError(id, err) { @@ -177,6 +173,7 @@ export async function loadDesignSystem( loadConfig: createLoader({ dependencies, legacy: true, + jiti, filepath, resolver, onError(id, err) { From 207d84c3d1cdc4c193efeded069266a265535112 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 13 Dec 2024 12:38:49 -0500 Subject: [PATCH 08/13] Try resolving CSS imports as relative before modules --- packages/tailwindcss-language-server/src/resolver/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/tailwindcss-language-server/src/resolver/index.ts b/packages/tailwindcss-language-server/src/resolver/index.ts index b57436fb..788f7410 100644 --- a/packages/tailwindcss-language-server/src/resolver/index.ts +++ b/packages/tailwindcss-language-server/src/resolver/index.ts @@ -105,6 +105,10 @@ export async function createResolver(opts: ResolverOptions): Promise { mainFields: ['style'], conditionNames: ['style'], pnpApi, + + // Given `foo/bar.css` try `./foo/bar.css` first before trying `foo/bar.css` + // as a module + preferRelative: true, }) async function resolveId( From f0be47e240793d7e3cc487fcffe61bf3a5202354 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 17:53:47 -0500 Subject: [PATCH 09/13] Add support for TypeScript config paths --- .../tailwindcss-language-server/package.json | 2 + .../src/project-locator.test.ts | 16 +- .../src/projects.ts | 3 + .../src/resolver/index.ts | 76 +++++ .../src/resolver/tsconfig.ts | 285 ++++++++++++++++++ .../tailwindcss-language-server/src/tw.ts | 1 + .../src/util/default-map.ts | 20 ++ .../tests/fixtures/v4/path-mappings/app.css | 5 + .../v4/path-mappings/package-lock.json | 17 ++ .../fixtures/v4/path-mappings/package.json | 5 + .../fixtures/v4/path-mappings/src/a/file.css | 3 + .../v4/path-mappings/src/a/my-config.ts | 11 + .../v4/path-mappings/src/a/my-plugin.ts | 17 ++ .../fixtures/v4/path-mappings/tsconfig.json | 7 + .../tests/hover/hover.test.js | 55 ++++ 15 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 packages/tailwindcss-language-server/src/resolver/tsconfig.ts create mode 100644 packages/tailwindcss-language-server/src/util/default-map.ts create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/app.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/file.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-config.ts create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-plugin.ts create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/tsconfig.json diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index 6ae216fe..1c772dc1 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -81,6 +81,8 @@ "rimraf": "3.0.2", "stack-trace": "0.0.10", "tailwindcss": "3.4.4", + "tsconfck": "^3.1.4", + "tsconfig-paths": "^4.2.0", "typescript": "5.3.3", "vite-tsconfig-paths": "^4.3.1", "vitest": "^1.4.0", diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 7261e736..25853d55 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -18,7 +18,7 @@ function testFixture(fixture: string, details: any[]) { let fixturePath = `${fixtures}/${fixture}` test.concurrent(fixture, async ({ expect }) => { - let resolver = await createResolver({ root: fixturePath }) + let resolver = await createResolver({ root: fixturePath, tsconfig: true }) let locator = new ProjectLocator(fixturePath, settings, resolver) let projects = await locator.search() @@ -205,3 +205,17 @@ testFixture('v4/missing-files', [ content: ['{URL}/package.json'], }, ]) + +testFixture('v4/path-mappings', [ + // + { + config: 'app.css', + content: [ + '{URL}/package.json', + '{URL}/src/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}', + '{URL}/src/a/my-config.ts', + '{URL}/src/a/my-plugin.ts', + '{URL}/tsconfig.json', + ], + }, +]) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 5d90f0b3..ea440c22 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -232,6 +232,7 @@ export async function createProjectService( async readDirectory(document, directory) { try { let baseDir = path.dirname(getFileFsPath(document.uri)) + directory = await resolver.substituteId(`${directory}/`, baseDir) directory = path.resolve(baseDir, directory) let dirents = await fs.promises.readdir(directory, { withFileTypes: true }) @@ -1163,6 +1164,8 @@ export async function createProjectService( let baseDir = path.dirname(documentPath) async function resolveTarget(linkPath: string) { + linkPath = (await resolver.substituteId(linkPath, baseDir)) ?? linkPath + return URI.file(path.resolve(baseDir, linkPath)).toString() } diff --git a/packages/tailwindcss-language-server/src/resolver/index.ts b/packages/tailwindcss-language-server/src/resolver/index.ts index 788f7410..b5c6db94 100644 --- a/packages/tailwindcss-language-server/src/resolver/index.ts +++ b/packages/tailwindcss-language-server/src/resolver/index.ts @@ -7,6 +7,7 @@ import { FileSystem, } from 'enhanced-resolve' import { loadPnPApi, type PnpApi } from './pnp' +import { loadTsConfig, type TSConfigApi } from './tsconfig' export interface ResolverOptions { /** @@ -23,6 +24,15 @@ export interface ResolverOptions { */ pnp?: boolean | PnpApi + /** + * Whether or not the resolver should load tsconfig path mappings. + * + * If `true`, the resolver will look for all `tsconfig` files in the project + * and use them to resolve module paths where possible. However, if an API is + * provided, the resolver will use that API to resolve module paths. + */ + tsconfig?: boolean | TSConfigApi + /** * A filesystem to use for resolution. If not provided, the resolver will * create one and use it internally for itself and any child resolvers that @@ -61,6 +71,23 @@ export interface Resolver { */ resolveCssId(id: string, base: string): Promise + /** + * Resolves a module to a possible file or directory path. + * + * This provides reasonable results when TypeScript config files are in use. + * This file may not exist but is the likely path that would be used to load + * the module if it were to exist. + * + * @param id The module, file, or directory to resolve + * @param base The base directory to resolve the module from + */ + substituteId(id: string, base: string): Promise + + /** + * Return a list of path resolution aliases for the given base directory + */ + aliases(base: string): Promise> + /** * Create a child resolver with the given options. * @@ -69,6 +96,13 @@ export interface Resolver { * the same PnP API without needing to load it again. */ child(opts: Partial): Promise + + /** + * Refresh information the resolver may have cached + * + * This may look for new TypeScript configs if necessary + */ + refresh(): Promise } export async function createResolver(opts: ResolverOptions): Promise { @@ -83,6 +117,22 @@ export async function createResolver(opts: ResolverOptions): Promise { pnpApi = await loadPnPApi(opts.root) } + let tsconfig: TSConfigApi | null = null + + // Load TSConfig path mappings + if (typeof opts.tsconfig === 'object') { + tsconfig = opts.tsconfig + } else if (opts.tsconfig) { + try { + tsconfig = await loadTsConfig(opts.root) + } catch (err) { + // We don't want to hard crash in case of an error handling tsconfigs + // It does affect what projects we can resolve or how we load files + // but the LSP shouldn't become unusable because of it. + console.error('Failed to load tsconfig', err) + } + } + let esmResolver = ResolverFactory.createResolver({ fileSystem, extensions: ['.mjs', '.js'], @@ -128,6 +178,11 @@ export async function createResolver(opts: ResolverOptions): Promise { if (base.startsWith('//')) base = `\\\\${base.slice(2)}` } + if (tsconfig) { + let match = await tsconfig.resolveId(id, base) + if (match) id = match + } + return new Promise((resolve, reject) => { resolver.resolve({}, base, id, {}, (err, res) => { if (err) { @@ -151,14 +206,34 @@ export async function createResolver(opts: ResolverOptions): Promise { return (await resolveId(cssResolver, id, base)) || id } + // Takes a path which may or may not be complete and returns the aliased path + // if possible + async function substituteId(id: string, base: string): Promise { + return (await tsconfig?.substituteId(id, base)) ?? id + } + async function setupPnP() { pnpApi?.setup() } + async function aliases(base: string) { + if (!tsconfig) return {} + + return await tsconfig.paths(base) + } + + async function refresh() { + await tsconfig?.refresh() + } + return { setupPnP, resolveJsId, resolveCssId, + substituteId, + refresh, + + aliases, child(childOpts: Partial) { return createResolver({ @@ -167,6 +242,7 @@ export async function createResolver(opts: ResolverOptions): Promise { // Inherit defaults from parent pnp: childOpts.pnp ?? pnpApi, + tsconfig: childOpts.tsconfig ?? tsconfig, fileSystem: childOpts.fileSystem ?? fileSystem, }) }, diff --git a/packages/tailwindcss-language-server/src/resolver/tsconfig.ts b/packages/tailwindcss-language-server/src/resolver/tsconfig.ts new file mode 100644 index 00000000..e578613f --- /dev/null +++ b/packages/tailwindcss-language-server/src/resolver/tsconfig.ts @@ -0,0 +1,285 @@ +// This implementation is inspired by and very loosely based on a Vite plugin +// with many simplifications and changes for our use case. +// +// The Vite plugin `vite-tsconfig-paths` can be found here: +// MIT License | Copyright (c) Alec Larson +// https://github.com/aleclarson/vite-tsconfig-paths + +import * as path from 'node:path' +import * as tsconfig from 'tsconfig-paths' +import * as tsconfck from 'tsconfck' +import { normalizePath } from '../utils' +import { DefaultMap } from '../util/default-map' + +export interface TSConfigApi { + /** + * Get the tsconfig paths used in the given directory + * + * @param base The directory to get the paths for + */ + paths(base: string): Promise> + + /** + * Resolve a module to a file path based on the tsconfig paths + * + * @param id The module or file to resolve + * @param base The directory to resolve the module from + */ + resolveId(id: string, base: string): Promise + + /** + * Given an id and base path turn it into a path that's likely to be + * the one that will can be used to load the module. + * + * @param id The module, file, or directory to resolve + * @param base The directory to resolve the module from + */ + substituteId(id: string, base: string): Promise + + /** + * Refresh information on available tsconfig paths. + * + * This rescans the project for tsconfig files and updates the matchers. + */ + refresh(): Promise + + /** + * Errors we encountered while trying to load the tsconfig files. + * + * We don't crash on errors because we want to be able to provide partial info + * even if some of the tsconfig files are invalid. + */ + errors: unknown[] +} + +export async function loadTsConfig(root: string): Promise { + let { configs, errors } = await findConfigs(root) + + let matchers = await createMatchers(configs) + + // 5. Create matchers for each project + async function resolveId(id: string, base: string) { + for (let projectDir of walkPaths(base)) { + for (let { match } of matchers.get(projectDir)) { + try { + return await match(id) + } catch (err) { + // If we got here we found a valid resolver for this path but it + // failed to resolve the path then we should stop looking. If we + // didn't we might end up using a resolver that would give us a + // valid path but not the one we want. + return null + } + } + } + + return null + } + + async function substituteId(id: string, base: string) { + for (let projectDir of walkPaths(base)) { + for (let { match } of matchers.get(projectDir)) { + try { + return await match(id, { mustExist: false }) + } catch (err) { + // If we got here we found a valid resolver for this path but it + // failed to resolve the path then we should stop looking. If we + // didn't we might end up using a resolver that would give us a + // valid path but not the one we want. + return null + } + } + } + + return null + } + + async function paths(base: string) { + for (let projectDir of walkPaths(base)) { + for (let { paths } of matchers.get(projectDir)) { + if (Object.keys(paths).length) return paths + } + } + + return {} + } + + async function refresh() { + let { configs, errors } = await findConfigs(root) + + matchers = await createMatchers(configs) + + if (errors.length) { + throw new AggregateError(errors) + } + } + + return { + resolveId, + substituteId, + paths, + refresh, + errors, + } +} + +async function findConfigs(root: string): Promise<{ + configs: Set + errors: unknown[] +}> { + // 1. Find all tsconfig files in the project + let files = await tsconfck.findAll(root, { + configNames: ['tsconfig.json', 'jsconfig.json'], + skip(dir) { + if (dir === 'node_modules') return true + if (dir === '.git') return true + + // TODO: Incorporate thee `exclude` option from VSCode settings. + // + // Doing so here is complicated because we don't have access to the + // full path to the file here and we need that to match it against the + // exclude patterns. + // + // This probably means we need to filter them after we've found them all. + + return false + }, + }) + + // 2. Load them all + let options: tsconfck.TSConfckParseOptions = { + root, + cache: new tsconfck.TSConfckCache(), + } + + let parsed = new Set() + let errors: unknown[] = [] + + for (let file of files) { + try { + let result = await tsconfck.parse(file, options) + parsed.add(result) + } catch (err) { + errors.push(err) + } + } + + // 3. Extract referenced projects + for (let result of parsed) { + if (!result.referenced) continue + + // Mach against referenced projects rather than the project itself + for (let ref of result.referenced) { + parsed.add(ref) + } + + // And use the project itself as a fallback since project references can + // be used to override the parent project. + parsed.delete(result) + parsed.add(result) + + result.referenced = undefined + } + + for (let err of errors) { + console.error(err) + } + + return { configs: parsed, errors } +} + +interface MatchOptions { + mustExist?: boolean +} + +interface Matcher { + match(id: string, opts?: MatchOptions): Promise + paths: Record +} + +async function createMatchers( + configs: Iterable, +): Promise> { + let matchers = new DefaultMap(() => []) + + let assumeExists: tsconfig.FileExistsAsync = (_, callback) => callback(undefined, true) + + for (let result of configs) { + let parent = normalizePath(path.dirname(result.tsconfigFile)) + + let opts = result.tsconfig.compilerOptions ?? {} + + let baseUrl = findBaseDir(result) + let absoluteBaseUrl = path.resolve(parent, baseUrl || '') + + let matchPath!: tsconfig.MatchPathAsync + + function match(id: string, { mustExist = true }: MatchOptions = {}) { + matchPath ??= tsconfig.createMatchPathAsync( + absoluteBaseUrl, + opts.paths ?? {}, + undefined, + baseUrl !== undefined, + ) + + let isPrefixMatch = mustExist === false ? id.endsWith('/') : false + + if (isPrefixMatch) { + id += '__placeholder__' + } + + return new Promise((resolve, reject) => { + matchPath( + id, + undefined, + mustExist === false ? assumeExists : undefined, + undefined, + (err, path) => { + if (err) return reject(err) + + if (isPrefixMatch) { + path = path.replace(/__placeholder__$/, '') + } + + return resolve(path) + }, + ) + }) + } + + matchers.get(parent).push({ + match, + paths: opts.paths ?? {}, + }) + } + + return matchers +} + +function* walkPaths(base: string) { + let projectDir = normalizePath(base) + + let prevProjectDir: string | undefined + while (projectDir !== prevProjectDir) { + yield projectDir + + prevProjectDir = projectDir + projectDir = path.dirname(projectDir) + } + + return null +} + +function findBaseDir(project: tsconfck.TSConfckParseResult): string { + let baseUrl = project.tsconfig.compilerOptions?.baseUrl + if (baseUrl) return baseUrl + + for (let p of project.extended ?? []) { + let opts = p.tsconfig.compilerOptions ?? {} + if (opts?.paths) { + return path.dirname(p.tsconfigFile) + } + } + + return path.dirname(project.tsconfigFile) +} diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 79d8b6cf..80e5b1e4 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -247,6 +247,7 @@ export class TW { let resolver = await createResolver({ root: base, pnp: true, + tsconfig: true, }) let locator = new ProjectLocator(base, globalSettings, resolver) diff --git a/packages/tailwindcss-language-server/src/util/default-map.ts b/packages/tailwindcss-language-server/src/util/default-map.ts new file mode 100644 index 00000000..a045b828 --- /dev/null +++ b/packages/tailwindcss-language-server/src/util/default-map.ts @@ -0,0 +1,20 @@ +/** + * A Map that can generate default values for keys that don't exist. + * Generated default values are added to the map to avoid recomputation. + */ +export class DefaultMap extends Map { + constructor(private factory: (key: T, self: DefaultMap) => V) { + super() + } + + get(key: T): V { + let value = super.get(key) + + if (value === undefined) { + value = this.factory(key, this) + this.set(key, value) + } + + return value + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/app.css b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/app.css new file mode 100644 index 00000000..a6aa8a2b --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/app.css @@ -0,0 +1,5 @@ +@import 'tailwindcss'; + +@import '#a/file.css'; +@config '#a/my-config.ts'; +@plugin '#a/my-plugin.ts'; diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json new file mode 100644 index 00000000..5de5cb3e --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "path-mappings", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0-beta.6.tgz", + "integrity": "sha512-eCCuMk3H65w4J/QWkjxfeWoBSKbCD3E6Uj2LA6Xkkl4eMa1MXuwVpIU1RXcLIp+BVsXGEZMP7i7uJig7KxfAXQ==" + } + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json new file mode 100644 index 00000000..960bfb88 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/file.css b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/file.css new file mode 100644 index 00000000..49426571 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/file.css @@ -0,0 +1,3 @@ +@theme { + --color-map-a-css: black; +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-config.ts b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-config.ts new file mode 100644 index 00000000..1f37e378 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-config.ts @@ -0,0 +1,11 @@ +import type { Config } from 'tailwindcss' + +export default { + theme: { + extend: { + colors: { + 'map-a-config': 'black', + }, + }, + }, +} satisfies Config diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-plugin.ts b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-plugin.ts new file mode 100644 index 00000000..933c9d32 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/src/a/my-plugin.ts @@ -0,0 +1,17 @@ +import type { PluginAPI } from 'tailwindcss' +import plugin from 'tailwindcss/plugin' + +export default plugin( + (api: PluginAPI) => { + // + }, + { + theme: { + extend: { + colors: { + 'map-a-plugin': 'black', + }, + }, + }, + }, +) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/tsconfig.json b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/tsconfig.json new file mode 100644 index 00000000..6e9c38fd --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "#a/*": ["./src/a/*"] + } + } +} diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 8967ab40..4ab6aeb8 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -357,3 +357,58 @@ withFixture('v4/css-loading-js', (c) => { }, }) }) + +withFixture('v4/path-mappings', (c) => { + async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { + test.concurrent(name, async ({ expect }) => { + let textDocument = await c.openDocument({ text, lang, settings }) + let res = await c.sendRequest('textDocument/hover', { + textDocument, + position, + }) + + expect(res).toEqual( + expected + ? { + contents: { + language: 'css', + value: expected, + }, + range: expectedRange, + } + : expected, + ) + }) + } + + testHover('Mapping: CSS Imports', { + text: '
', + position: { line: 0, character: 13 }, + expected: + '.bg-map-a-css {\n background-color: var(--color-map-a-css) /* black = #000000 */;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 24 }, + }, + }) + + testHover('Mapping: Configs', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-map-a-config {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 27 }, + }, + }) + + testHover('Mapping: Plugins', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-map-a-plugin {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 27 }, + }, + }) +}) From 6bbcb6bb2fa0cb7df67de67fa097303351703839 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 9 Jan 2025 10:16:12 -0500 Subject: [PATCH 10/13] Reload when tsconfig files change --- .../src/lib/constants.ts | 1 + .../tailwindcss-language-server/src/tw.ts | 26 +++++++++++++++++-- .../src/util/retry.ts | 19 ++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/util/retry.ts diff --git a/packages/tailwindcss-language-server/src/lib/constants.ts b/packages/tailwindcss-language-server/src/lib/constants.ts index 29d0e641..3ad6d2fa 100644 --- a/packages/tailwindcss-language-server/src/lib/constants.ts +++ b/packages/tailwindcss-language-server/src/lib/constants.ts @@ -2,3 +2,4 @@ export const CONFIG_GLOB = '{tailwind,tailwind.config,tailwind.*.config,tailwind.config.*}.{js,cjs,ts,mjs,mts,cts}' export const PACKAGE_LOCK_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' export const CSS_GLOB = '*.{css,scss,sass,less,pcss}' +export const TSCONFIG_GLOB = '{tsconfig,tsconfig.*,jsconfig,jsconfig.*}.json' diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 80e5b1e4..e5d6522f 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -39,7 +39,7 @@ 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 } from './lib/constants' +import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB, TSCONFIG_GLOB } from './lib/constants' import { clearRequireCache, isObject, changeAffectsFile, normalizeDriveLetter } from './utils' import { DocumentService } from './documents' import { createProjectService, type ProjectService } from './projects' @@ -48,6 +48,7 @@ import { readCssFile } from './util/css' import { ProjectLocator, type ProjectConfig } from './project-locator' import type { TailwindCssSettings } from '@tailwindcss/language-service/src/util/state' import { createResolver, Resolver } from './resolver' +import { retry } from './util/retry' const TRIGGER_CHARACTERS = [ // class attributes @@ -296,6 +297,7 @@ export class TW { let isPackageMatcher = picomatch(`**/${PACKAGE_LOCK_GLOB}`, { dot: true }) let isCssMatcher = picomatch(`**/${CSS_GLOB}`, { dot: true }) let isConfigMatcher = picomatch(`**/${CONFIG_GLOB}`, { dot: true }) + let isTSConfigMatcher = picomatch(`**/${TSCONFIG_GLOB}`, { dot: true }) changeLoop: for (let change of changes) { let normalizedFilename = normalizePath(change.file) @@ -335,6 +337,25 @@ export class TW { } } + let isTsconfig = isTSConfigMatcher(normalizedFilename) + if (isTsconfig) { + // TODO: Use a refresh() instead of a full server restart + // let refreshPromise = retry({ + // tries: 4, + // delay: 250, + // callback: () => resolver.refresh(), + // }) + + // try { + // await refreshPromise + // } catch (err) { + // console.error('Unable to reload resolver', err) + // } + + needsRestart = true + break changeLoop + } + for (let [, project] of this.projects) { if (!project.state.v4) continue @@ -424,6 +445,7 @@ export class TW { { globPattern: `**/${CONFIG_GLOB}` }, { globPattern: `**/${PACKAGE_LOCK_GLOB}` }, { globPattern: `**/${CSS_GLOB}` }, + { globPattern: `**/${TSCONFIG_GLOB}` }, ], }, ) @@ -472,7 +494,7 @@ export class TW { } else { let watch: typeof chokidar.watch = require('chokidar').watch let chokidarWatcher = watch( - [`**/${CONFIG_GLOB}`, `**/${PACKAGE_LOCK_GLOB}`, `**/${CSS_GLOB}`], + [`**/${CONFIG_GLOB}`, `**/${PACKAGE_LOCK_GLOB}`, `**/${CSS_GLOB}`, `**/${TSCONFIG_GLOB}`], { cwd: base, ignorePermissionErrors: true, diff --git a/packages/tailwindcss-language-server/src/util/retry.ts b/packages/tailwindcss-language-server/src/util/retry.ts new file mode 100644 index 00000000..b14b927a --- /dev/null +++ b/packages/tailwindcss-language-server/src/util/retry.ts @@ -0,0 +1,19 @@ +export interface RetryOptions { + tries: number + delay: number + callback: () => Promise +} + +export async function retry({ tries, delay, callback }) { + retry: try { + return await callback() + } catch (err) { + if (tries-- === 0) throw err + + // Wait a bit before trying again _ this exists for projects like + // Nuxt that create a several tsconfig files at once + await new Promise((resolve) => setTimeout(resolve, delay)) + + break retry + } +} From fa9e0fea40411513f3ce689b73045e699db98244 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 14 Dec 2024 15:50:02 -0500 Subject: [PATCH 11/13] Update lockfile --- pnpm-lock.yaml | 377 +++++++++++++++++++++++++++---------------------- 1 file changed, 208 insertions(+), 169 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0ca5d41..596c70aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^4.0.1 version: 4.0.1 esbuild: - specifier: ^0.20.2 - version: 0.20.2 + specifier: ^0.24.0 + version: 0.24.0 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -123,8 +123,8 @@ importers: specifier: ^5.16.1 version: 5.17.1 esbuild: - specifier: ^0.20.2 - version: 0.20.2 + specifier: ^0.24.0 + version: 0.24.0 fast-glob: specifier: 3.2.4 version: 3.2.4 @@ -176,6 +176,12 @@ importers: tailwindcss: specifier: 3.4.4 version: 3.4.4 + tsconfck: + specifier: ^3.1.4 + version: 3.1.4(typescript@5.3.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 typescript: specifier: 5.3.3 version: 5.3.3 @@ -298,11 +304,11 @@ importers: specifier: ^4.0.5 version: 4.0.5 esbuild: - specifier: ^0.20.2 - version: 0.20.2 + specifier: ^0.24.0 + version: 0.24.0 esbuild-node-externals: specifier: ^1.9.0 - version: 1.14.0(esbuild@0.20.2) + version: 1.14.0(esbuild@0.24.0) minimist: specifier: ^1.2.8 version: 1.2.8 @@ -346,8 +352,8 @@ importers: specifier: 7.0.0 version: 7.0.0 esbuild: - specifier: ^0.20.2 - version: 0.20.2 + specifier: ^0.24.0 + version: 0.24.0 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -409,23 +415,17 @@ packages: '@csstools/css-parser-algorithms': ^2.1.1 '@csstools/css-tokenizer': ^2.1.1 - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} @@ -433,10 +433,10 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] os: [android] '@esbuild/android-arm@0.21.5': @@ -445,10 +445,10 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] os: [android] '@esbuild/android-x64@0.21.5': @@ -457,11 +457,11 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} @@ -469,10 +469,10 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.21.5': @@ -481,11 +481,11 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} @@ -493,10 +493,10 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.21.5': @@ -505,11 +505,11 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} @@ -517,10 +517,10 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.21.5': @@ -529,10 +529,10 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.21.5': @@ -541,10 +541,10 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.21.5': @@ -553,10 +553,10 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.21.5': @@ -565,10 +565,10 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.21.5': @@ -577,10 +577,10 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.21.5': @@ -589,10 +589,10 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.21.5': @@ -601,10 +601,10 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.21.5': @@ -613,11 +613,11 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} cpu: [x64] - os: [netbsd] + os: [linux] '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} @@ -625,10 +625,16 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.21.5': @@ -637,11 +643,11 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} cpu: [x64] - os: [sunos] + os: [openbsd] '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} @@ -649,11 +655,11 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} @@ -661,10 +667,10 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.21.5': @@ -673,10 +679,10 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.21.5': @@ -685,6 +691,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1382,16 +1394,16 @@ packages: peerDependencies: esbuild: 0.12 - 0.23 - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -1667,6 +1679,11 @@ packages: resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -2378,6 +2395,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -2475,8 +2496,8 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tsconfck@3.1.1: - resolution: {integrity: sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==} + tsconfck@3.1.4: + resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -2485,6 +2506,10 @@ packages: typescript: optional: true + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -2782,144 +2807,147 @@ snapshots: '@csstools/css-parser-algorithms': 2.1.1(@csstools/css-tokenizer@2.1.1) '@csstools/css-tokenizer': 2.1.1 - '@esbuild/aix-ppc64@0.20.2': - optional: true - '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/android-arm64@0.20.2': + '@esbuild/aix-ppc64@0.24.0': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm@0.20.2': + '@esbuild/android-arm64@0.24.0': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-x64@0.20.2': + '@esbuild/android-arm@0.24.0': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.20.2': + '@esbuild/android-x64@0.24.0': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-x64@0.20.2': + '@esbuild/darwin-arm64@0.24.0': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.20.2': + '@esbuild/darwin-x64@0.24.0': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.20.2': + '@esbuild/freebsd-arm64@0.24.0': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/linux-arm64@0.20.2': + '@esbuild/freebsd-x64@0.24.0': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm@0.20.2': + '@esbuild/linux-arm64@0.24.0': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-ia32@0.20.2': + '@esbuild/linux-arm@0.24.0': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-loong64@0.20.2': + '@esbuild/linux-ia32@0.24.0': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-mips64el@0.20.2': + '@esbuild/linux-loong64@0.24.0': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-ppc64@0.20.2': + '@esbuild/linux-mips64el@0.24.0': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.20.2': + '@esbuild/linux-ppc64@0.24.0': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-s390x@0.20.2': + '@esbuild/linux-riscv64@0.24.0': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-x64@0.20.2': + '@esbuild/linux-s390x@0.24.0': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.20.2': + '@esbuild/linux-x64@0.24.0': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.20.2': + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.20.2': + '@esbuild/openbsd-x64@0.24.0': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/win32-arm64@0.20.2': + '@esbuild/sunos-x64@0.24.0': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-ia32@0.20.2': + '@esbuild/win32-arm64@0.24.0': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-x64@0.20.2': + '@esbuild/win32-ia32@0.24.0': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.24.0': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3598,38 +3626,12 @@ snapshots: es-errors@1.3.0: {} - esbuild-node-externals@1.14.0(esbuild@0.20.2): + esbuild-node-externals@1.14.0(esbuild@0.24.0): dependencies: - esbuild: 0.20.2 + esbuild: 0.24.0 find-up: 5.0.0 tslib: 2.6.3 - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -3656,6 +3658,33 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + escalade@3.1.2: {} escape-string-regexp@1.0.5: {} @@ -3913,6 +3942,8 @@ snapshots: json-parse-even-better-errors@3.0.0: {} + json5@2.2.3: {} + jsonc-parser@3.3.1: {} keytar@7.9.0: @@ -4660,6 +4691,8 @@ snapshots: dependencies: ansi-regex: 6.0.1 + strip-bom@3.0.0: {} + strip-final-newline@3.0.0: {} strip-indent@4.0.0: @@ -4773,10 +4806,16 @@ snapshots: ts-interface-checker@0.1.13: {} - tsconfck@3.1.1(typescript@5.3.3): + tsconfck@3.1.4(typescript@5.3.3): optionalDependencies: typescript: 5.3.3 + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@1.14.1: {} tslib@2.2.0: {} @@ -4854,7 +4893,7 @@ snapshots: dependencies: debug: 4.3.6 globrex: 0.1.2 - tsconfck: 3.1.1(typescript@5.3.3) + tsconfck: 3.1.4(typescript@5.3.3) optionalDependencies: vite: 5.3.5(@types/node@18.19.43) transitivePeerDependencies: From fc28aca2c2bb1507d8383c29419a7ef50e40e214 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 16:05:40 -0500 Subject: [PATCH 12/13] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 06dfcf83..d59e1e84 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,9 @@ ## Prerelease -- Nothing yet! +- Don't break when importing missing CSS files ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) +- Resolve CSS imports as relative first ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) +- Add TypeScript config path support in v4 CSS files ([#1106](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1106)) ## 0.12.18 From 4d9917c50a2b8f436e9f77f7a2e7855d2416202f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 8 Jan 2025 12:18:32 -0500 Subject: [PATCH 13/13] Hoist `@import` nodes to the top in the project locator This ensures project initialization can proceed far enough that Tailwind CSS itself should attempt to initalize. It also means we can reliably detect this case and show an error in the console. --- .../src/css/resolve-css-imports.ts | 29 +++++++++++++++++++ .../src/project-locator.test.ts | 8 +++++ .../src/project-locator.ts | 11 ++++--- .../fixtures/v4/invalid-import-order/a.css | 5 ++++ .../fixtures/v4/invalid-import-order/b.css | 5 ++++ .../v4/invalid-import-order/package-lock.json | 17 +++++++++++ .../v4/invalid-import-order/package.json | 5 ++++ .../v4/invalid-import-order/tailwind.css | 14 +++++++++ 8 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/a.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/b.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css diff --git a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts index 31b598c9..7cfd7f3f 100644 --- a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts +++ b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts @@ -6,10 +6,39 @@ import { Resolver } from '../resolver' export function resolveCssImports({ resolver, + loose = false, }: { resolver: Resolver + loose?: boolean }) { return postcss([ + // Hoist imports to the top of the file + { + postcssPlugin: 'hoist-at-import', + Once(root, { result }) { + if (!loose) return + + let hoist: postcss.AtRule[] = [] + let seenImportsAfterOtherNodes = false + + for (let node of root.nodes) { + if (node.type === 'atrule' && (node.name === 'import' || node.name === 'charset')) { + hoist.push(node) + } else if (hoist.length > 0 && (node.type === 'atrule' || node.type === 'rule')) { + seenImportsAfterOtherNodes = true + } + } + + root.prepend(hoist) + + if (!seenImportsAfterOtherNodes) return + + console.log( + `hoist-at-import: The file '${result.opts.from}' contains @import rules after other at rules. This is invalid CSS and may cause problems with your build.`, + ) + }, + }, + postcssImport({ async resolve(id, base) { try { diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 25853d55..fd805806 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -219,3 +219,11 @@ testFixture('v4/path-mappings', [ ], }, ]) + +testFixture('v4/invalid-import-order', [ + // + { + config: 'tailwind.css', + content: ['{URL}/package.json'], + }, +]) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index e12927db..08ba66fa 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -618,7 +618,9 @@ class FileEntry { async resolveImports(resolver: Resolver) { try { - let result = await resolveCssImports({ resolver }).process(this.content, { from: this.path }) + let result = await resolveCssImports({ resolver, loose: true }).process(this.content, { + from: this.path, + }) let deps = result.messages.filter((msg) => msg.type === 'dependency') deps = deps.filter((msg) => { @@ -630,9 +632,10 @@ class FileEntry { // Replace the file content with the processed CSS this.content = result.css - } catch { - // TODO: Errors here should be surfaced in tests and possibly the user in - // `trace` logs or something like that + } catch (err) { + console.debug(`Unable to resolve imports for ${this.path}.`) + console.debug(`This may result in failure to locate Tailwind CSS projects.`) + console.error(err) } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/a.css b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/a.css new file mode 100644 index 00000000..b60fe2c7 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/a.css @@ -0,0 +1,5 @@ +@layer base { + :root { + font-family: sans-serif; + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/b.css b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/b.css new file mode 100644 index 00000000..62948aff --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/b.css @@ -0,0 +1,5 @@ +@layer base { + :root { + --foo: red; + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json new file mode 100644 index 00000000..47224857 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "invalid-import-order", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0-beta.6.tgz", + "integrity": "sha512-eCCuMk3H65w4J/QWkjxfeWoBSKbCD3E6Uj2LA6Xkkl4eMa1MXuwVpIU1RXcLIp+BVsXGEZMP7i7uJig7KxfAXQ==" + } + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json new file mode 100644 index 00000000..960bfb88 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tailwindcss": "^4.0.0-beta.6" + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css new file mode 100644 index 00000000..e3d2eb5e --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/tailwind.css @@ -0,0 +1,14 @@ +@import 'tailwindcss'; + +/* + * This is invalid in this position because some `@import`s are not at the top of the file. + * We don't want project discovery to fail so we hoist them up and then warn in the console. + */ +@variant dark (&:where(.dark, .dark *)); + +@import './a.css'; +@import './b.css'; + +@theme { + --color-primary: #c0ffee; +}