From cf97d7a43dc73e2aa71edc52f5cc567334d94842 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 26 Mar 2024 11:21:37 -0400 Subject: [PATCH] Handle `style` exports condition when processing `@import`s (#934) * Refactor * Support customizing directory of an opened completion document * Add notes * Resolve `style` exports field in postcss import * Update changelog * Add test * Refactor * Make sure the main `style` field is still supported --- .../src/project-locator.test.ts | 1 + .../src/project-locator.ts | 6 +-- .../src/projects.ts | 1 + .../src/resolve-css-imports.ts | 24 +++++++++++ .../src/util/resolve.ts | 16 +++++++ .../src/util/resolveFrom.ts | 26 +++--------- .../src/util/v4/design-system.ts | 6 +-- .../tests/common.ts | 2 + .../tests/completions/completions.test.js | 42 ++++++++++++++++++- .../fixtures/v4/workspaces/package-lock.json | 12 ++++++ .../workspaces/packages/style-export/lib.css | 3 ++ .../packages/style-export/package.json | 9 ++++ .../packages/style-export/theme.css | 3 ++ .../packages/style-main-field/lib.css | 3 ++ .../packages/style-main-field/package.json | 4 ++ .../v4/workspaces/packages/web/app.css | 4 ++ packages/vscode-tailwindcss/CHANGELOG.md | 1 + 17 files changed, 134 insertions(+), 29 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/resolve-css-imports.ts create mode 100644 packages/tailwindcss-language-server/src/util/resolve.ts create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/lib.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/package.json create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/theme.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/lib.css create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/package.json diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 1b6aa0e9..0c443b8e 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -81,5 +81,6 @@ testFixture('v4/multi-config', [ testFixture('v4/workspaces', [ { config: 'packages/admin/app.css' }, // { config: 'packages/shared/ui.css' }, // Should this be included? + // { config: 'packages/style-export/lib.css' }, // Should this be included? { config: 'packages/web/app.css' }, ]) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 2af52862..ad5223f6 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -9,14 +9,13 @@ import { CONFIG_GLOB, CSS_GLOB } from './lib/constants' import { readCssFile } from './util/css' import { Graph } from './graph' import type { Message } from 'postcss' -import postcss from 'postcss' -import postcssImport from 'postcss-import' 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 Feature, supportedFeatures } from '@tailwindcss/language-service/src/features' import { pathToFileURL } from 'node:url' +import { resolveCssImports } from './resolve-css-imports' export interface ProjectConfig { /** The folder that contains the project */ @@ -483,7 +482,6 @@ type ConfigEntry = { content: ContentItem[] } -let resolveImports = postcss([postcssImport()]) class FileEntry { content: string | null deps: Message[] = [] @@ -504,7 +502,7 @@ class FileEntry { async resolveImports() { try { - let result = await resolveImports.process(this.content, { from: this.path }) + let result = await resolveCssImports().process(this.content, { from: this.path }) this.deps = result.messages.filter((msg) => msg.type === 'dependency') // Replace the file content with the processed CSS diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 0d923154..f5d18d87 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -755,6 +755,7 @@ export async function createProjectService( originalConfig = { theme: {} } } catch { // TODO: Fall back to built-in v4 stuff + // TODO: Log errors? It might get noisy as people are editing their CSS though enabled = false state.enabled = false return diff --git a/packages/tailwindcss-language-server/src/resolve-css-imports.ts b/packages/tailwindcss-language-server/src/resolve-css-imports.ts new file mode 100644 index 00000000..810af777 --- /dev/null +++ b/packages/tailwindcss-language-server/src/resolve-css-imports.ts @@ -0,0 +1,24 @@ +import postcss from 'postcss' +import postcssImport from 'postcss-import' +import { createResolver } from './util/resolve' + +const resolver = createResolver({ + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], +}) + +const resolveImports = postcss([ + postcssImport({ + resolve(id, basedir) { + let paths = resolver.resolveSync({}, basedir, id) + return paths + ? paths + : id + }, + }), +]) + +export function resolveCssImports() { + return resolveImports +} diff --git a/packages/tailwindcss-language-server/src/util/resolve.ts b/packages/tailwindcss-language-server/src/util/resolve.ts new file mode 100644 index 00000000..9f65d78b --- /dev/null +++ b/packages/tailwindcss-language-server/src/util/resolve.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs' +import { + CachedInputFileSystem, + ResolverFactory, + Resolver, + ResolveOptions, +} from 'enhanced-resolve-301' + +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 2d679ee4..6d378c2e 100644 --- a/packages/tailwindcss-language-server/src/util/resolveFrom.ts +++ b/packages/tailwindcss-language-server/src/util/resolveFrom.ts @@ -1,32 +1,18 @@ -import * as fs from 'fs' -import { - CachedInputFileSystem, - ResolverFactory, - Resolver, - ResolveOptions, -} from 'enhanced-resolve-301' import { equal } from '@tailwindcss/language-service/src/util/array' +import { createResolver } from './resolve' let pnpApi: any let extensions = Object.keys(require.extensions) -function createResolver(options: Partial = {}): Resolver { - return ResolverFactory.createResolver({ - fileSystem: new CachedInputFileSystem(fs, 4000), - useSyncFileSystemCalls: true, - // cachePredicate: () => false, - conditionNames: ['node', 'require'], - extensions, - pnpApi, - ...options, - }) +function recreateResolver() { + return createResolver({ extensions, pnpApi }) } -let resolver = createResolver() +let resolver = recreateResolver() export function setPnpApi(newPnpApi: any): void { pnpApi = newPnpApi - resolver = createResolver() + resolver = recreateResolver() } export default function resolveFrom(from?: string, id?: string): string { @@ -35,7 +21,7 @@ export default function resolveFrom(from?: string, id?: string): string { let newExtensions = Object.keys(require.extensions) if (!equal(newExtensions, extensions)) { extensions = newExtensions - resolver = createResolver() + resolver = recreateResolver() } let result = resolver.resolveSync({}, from, 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 d3d27ecb..0907af85 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -1,9 +1,7 @@ import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4' import postcss from 'postcss' -import postcssImport from 'postcss-import' - -const resolveImports = postcss([postcssImport()]) +import { resolveCssImports } from '../../resolve-css-imports' const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/ const HAS_V4_THEME = /@theme\s*\{/ @@ -36,7 +34,7 @@ export async function loadDesignSystem( // Step 2: Use postcss to resolve `@import` rules in the CSS file // TODO: What if someone is actively editing their config and introduces a syntax error? // We don't want to necessarily throw away the knowledge that we have a v4 project. - let resolved = await resolveImports.process(css, { from: filepath }) + let resolved = await resolveCssImports().process(css, { from: filepath }) // Step 3: Take the resolved CSS and pass it to v4's `loadDesignSystem` let design = tailwindcss.__unstable__loadDesignSystem(resolved.css) as DesignSystem diff --git a/packages/tailwindcss-language-server/tests/common.ts b/packages/tailwindcss-language-server/tests/common.ts index 16df7b2a..ece8e910 100644 --- a/packages/tailwindcss-language-server/tests/common.ts +++ b/packages/tailwindcss-language-server/tests/common.ts @@ -201,6 +201,8 @@ async function init(fixture: string): Promise { }, } as DidOpenTextDocumentParams) + // If opening a document stalls then it's probably because this promise is not being resolved + // This can happen if a document is not covered by one of the selectors because of it's URI await initPromise return { diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 656841a5..b9efbd71 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -10,8 +10,9 @@ function buildCompletion(c) { triggerKind: 1, }, settings, + dir = '', }) { - let textDocument = await c.openDocument({ text, lang, settings }) + let textDocument = await c.openDocument({ text, lang, settings, dir }) return c.sendRequest('textDocument/completion', { textDocument, @@ -552,3 +553,42 @@ withFixture('v4/basic', (c) => { }) }) }) + +withFixture('v4/workspaces', (c) => { + let completion = buildCompletion(c) + + test('@import resolution supports exports.style', async ({ expect }) => { + let result = await completion({ + dir: 'packages/web', + lang: 'html', + text: '
', + position: { line: 0, character: 12 }, + }) + + let items = [ + result.items.find((item) => item.label === 'bg-beet'), + result.items.find((item) => item.label === 'bg-orangepeel'), + result.items.find((item) => item.label === 'bg-style-main'), + ] + + let resolved = await Promise.all(items.map((item) => c.sendRequest('completionItem/resolve', item))) + + expect(resolved[0]).toEqual({ + ...items[0], + detail: 'background-color: #8e3b46;', + documentation: '#8e3b46', + }) + + expect(resolved[1]).toEqual({ + ...items[1], + detail: 'background-color: #ff9f00;', + documentation: '#ff9f00', + }) + + expect(resolved[2]).toEqual({ + ...items[2], + detail: 'background-color: #8e3b46;', + documentation: '#8e3b46', + }) + }) +}) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json index b99a222f..44968e60 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json @@ -19,6 +19,14 @@ "resolved": "packages/shared", "link": true }, + "node_modules/@private/style-export": { + "resolved": "packages/style-export", + "link": true + }, + "node_modules/@private/style-main-field": { + "resolved": "packages/style-main-field", + "link": true + }, "node_modules/@private/web": { "resolved": "packages/web", "link": true @@ -34,6 +42,10 @@ "packages/shared": { "name": "@private/shared" }, + "packages/style-export": { + "name": "@private/style-export" + }, + "packages/style-main-field": {}, "packages/web": { "name": "@private/web" } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/lib.css b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/lib.css new file mode 100644 index 00000000..5e41716b --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/lib.css @@ -0,0 +1,3 @@ +@theme { + --color-beet: #8e3b46; +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/package.json new file mode 100644 index 00000000..59f1f20a --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/package.json @@ -0,0 +1,9 @@ +{ + "name": "@private/style-export", + "exports": { + ".": { + "style": "./lib.css" + }, + "./theme": "./theme.css" + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/theme.css b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/theme.css new file mode 100644 index 00000000..7086a67e --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-export/theme.css @@ -0,0 +1,3 @@ +@theme { + --color-orangepeel: #ff9f00; +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/lib.css b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/lib.css new file mode 100644 index 00000000..b1a51b59 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/lib.css @@ -0,0 +1,3 @@ +@theme { + --color-style-main: #8e3b46; +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/package.json new file mode 100644 index 00000000..3d4cee4f --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/style-main-field/package.json @@ -0,0 +1,4 @@ +{ + "name": "@private/style-main-field", + "style": "./lib.css" +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/app.css b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/app.css index 3b443ee4..9caf9d44 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/app.css +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/packages/web/app.css @@ -1,5 +1,9 @@ @import 'tailwindcss'; @import '@private/shared/ui.css'; +@import '@private/style-export'; +@import '@private/style-export/theme'; +@import '@private/style-main-field'; + @theme { --color-potato: #907a70; } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 75602d67..71965803 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix hovers and CSS conflict detection in Vue `