Skip to content

Support loading plugins in CSS #1044

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-server/src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Module from 'node:module'
import * as path from 'node:path'
import resolveFrom from '../util/resolveFrom'
import { resolveFrom } from '../util/resolveFrom'

process.env.TAILWIND_MODE = 'build'
process.env.TAILWIND_DISABLE_TOUCH = 'true'
Expand Down
35 changes: 20 additions & 15 deletions packages/tailwindcss-language-server/src/project-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 { resolveFrom } from './util/resolveFrom'
import { type Feature, supportedFeatures } from '@tailwindcss/language-service/src/features'
import { extractSourceDirectives, resolveCssImports } from './css'
import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils'
Expand Down Expand Up @@ -132,15 +132,18 @@ export class ProjectLocator {

console.log(JSON.stringify({ tailwind }))

// A JS/TS config file was loaded from an `@config`` directive in a CSS file
// 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
// to verify if this is supported in the current version of Tailwind.
if (config.type === 'js' && config.source === 'css') {
// We only allow local versions of Tailwind to use `@config` directives
if (tailwind.isDefaultVersion) {
return null
}

// This version of Tailwind doesn't support `@config` directives
if (!tailwind.features.includes('css-at-config')) {
// This version of Tailwind doesn't support considering `@config` directives
// as a project on their own.
if (!tailwind.features.includes('css-at-config-as-project')) {
return null
}
}
Expand Down Expand Up @@ -310,8 +313,12 @@ export class ProjectLocator {
// If the CSS file couldn't be read for some reason, skip it
if (!file.content) continue

// Look for `@import`, `@tailwind`, `@theme`, `@config`, etc…
if (!file.isMaybeTailwindRelated()) continue

// Find `@config` directives in CSS files and resolve them to the actual
// config file that they point to.
// config file that they point to. This is only relevant for v3 which
// we'll verify after config resolution.
let configPath = file.configPathInCss()
if (configPath) {
// We don't need the content for this file anymore
Expand All @@ -327,14 +334,9 @@ export class ProjectLocator {
content: [],
})),
)
continue
}

// Look for `@import` or `@tailwind` directives
if (file.isMaybeTailwindRelated()) {
imports.push(file)
continue
}
imports.push(file)
}

// Resolve imports in all the CSS files
Expand Down Expand Up @@ -636,6 +638,9 @@ class FileEntry {
* Look for `@config` directives in a CSS file and return the path to the config
* file that it points to. This path is (possibly) relative to the CSS file so
* it must be resolved to an absolute path before returning.
*
* This is only useful for v3 projects. While v4 can use `@config` directives
* the CSS file is still considered the "config" rather than the JS file.
*/
configPathInCss(): string | null {
if (!this.content) return null
Expand All @@ -649,21 +654,21 @@ class FileEntry {
}

/**
* Look for `@import` or `@tailwind` directives in a CSS file. This means that
* Look for tailwind-specific directives in a CSS file. This means that it
* participates in the CSS "graph" for the project and we need to traverse
* the graph to find all the CSS files that are considered entrypoints.
*/
isMaybeTailwindRelated(): boolean {
if (!this.content) return false

let HAS_IMPORT = /@import\s*(?<config>'[^']+'|"[^"]+");/
let HAS_IMPORT = /@import\s*('[^']+'|"[^"]+");/
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
let HAS_THEME = /@theme\s*\{/
let HAS_DIRECTIVE = /@(theme|plugin|config|utility|variant|apply)\s*[^;{]+[;{]/

return (
HAS_IMPORT.test(this.content) ||
HAS_TAILWIND.test(this.content) ||
HAS_THEME.test(this.content)
HAS_DIRECTIVE.test(this.content)
)
}
}
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-server/src/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as path from 'path'
import * as fs from 'fs'
import findUp from 'find-up'
import picomatch from 'picomatch'
import resolveFrom, { setPnpApi } from './util/resolveFrom'
import { resolveFrom, setPnpApi } from './util/resolveFrom'
import type { AtRule, Container, Node, Result } from 'postcss'
import Hook from './lib/hook'
import * as semver from '@tailwindcss/language-service/src/util/semver'
Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import normalizePath from 'normalize-path'
import * as path from 'path'
import type * as chokidar from 'chokidar'
import picomatch from 'picomatch'
import resolveFrom from './util/resolveFrom'
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function setPnpApi(newPnpApi: any): void {
resolver = recreateResolver()
}

export default function resolveFrom(from?: string, id?: string): string {
export function resolveFrom(from?: string, id?: string): string {
// Network share path on Windows
if (id.startsWith('\\\\')) return id

Expand Down
54 changes: 51 additions & 3 deletions packages/tailwindcss-language-server/src/util/v4/design-system.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4'

import postcss from 'postcss'
import * as path from 'node:path'
import { resolveCssImports } from '../../css'
import { resolveFrom } from '../resolveFrom'
import { pathToFileURL } from 'tailwindcss-language-server/src/utils'

const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
const HAS_V4_THEME = /@theme\s*\{/
Expand All @@ -18,6 +21,37 @@ export async function isMaybeV4(css: string): Promise<boolean> {
return HAS_V4_THEME.test(css) || HAS_V4_IMPORT.test(css)
}

/**
* 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
* everything from working so we'll let the error handler decide how to proceed.
*
* @param {object} param0
* @returns
*/
function createLoader<T>({
filepath,
onError,
}: {
filepath: string
onError: (id: string, error: unknown) => T
}) {
let baseDir = path.dirname(filepath)
let cacheKey = `${+Date.now()}`

return async function loadFile(id: string) {
try {
let resolved = resolveFrom(baseDir, id)
let url = pathToFileURL(resolved)
url.searchParams.append('t', cacheKey)

return await import(url.href).then((m) => m.default ?? m)
} catch (err) {
return onError(id, err)
}
}
}

export async function loadDesignSystem(
tailwindcss: any,
filepath: string,
Expand All @@ -38,9 +72,23 @@ export async function loadDesignSystem(

// Step 3: Take the resolved CSS and pass it to v4's `loadDesignSystem`
let design: DesignSystem = await tailwindcss.__unstable__loadDesignSystem(resolved.css, {
loadPlugin() {
return () => {}
},
loadPlugin: createLoader({
filepath,
onError(id, err) {
console.error(`Unable to load plugin: ${id}`, err)

return () => {}
},
}),

loadConfig: createLoader({
filepath,
onError(id, err) {
console.error(`Unable to load config: ${id}`, err)

return {}
},
}),
})

// Step 4: Augment the design system with some additional APIs that the LSP needs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import 'tailwindcss';

/* Load ESM versions */
@config './esm/my-config.mjs';
@plugin './esm/my-plugin.mjs';

/* Load Common JS versions */
@config './cjs/my-config.cjs';
@plugin './cjs/my-plugin.cjs';

/* Load TypeScript versions */
@config './ts/my-config.ts';
@plugin './ts/my-plugin.ts';

/* Attempt to load files that do not exist */
@config './missing-confg.mjs';
@plugin './missing-plugin.mjs';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
theme: {
extend: {
colors: {
'cjs-from-config': 'black',
},
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const plugin = require('tailwindcss/plugin')

module.exports = plugin(
() => {
//
},
{
theme: {
extend: {
colors: {
'cjs-from-plugin': 'black',
},
},
},
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
theme: {
extend: {
colors: {
'esm-from-config': 'black',
},
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import plugin from 'tailwindcss/plugin'

export default plugin(
() => {
//
},
{
theme: {
extend: {
colors: {
'esm-from-plugin': 'black',
},
},
},
},
)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"tailwindcss": "file:tailwindcss.tgz"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Config } from 'tailwindcss'

export default {
theme: {
extend: {
colors: {
'ts-from-config': 'black',
},
},
},
} satisfies Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { PluginAPI } from 'tailwindcss'
import plugin from 'tailwindcss/plugin'

export default plugin(
(api: PluginAPI) => {
//
},
{
theme: {
extend: {
colors: {
'ts-from-plugin': 'black',
},
},
},
},
)
Loading