Skip to content

Commit

Permalink
Support loading plugins in CSS (#1044)
Browse files Browse the repository at this point in the history
This adds support for `@plugin` and `@config` in v4 which is coming in
v4.0.0-alpha.21
  • Loading branch information
thecrypticace authored Sep 2, 2024
1 parent 3c3c193 commit c2c7cc7
Show file tree
Hide file tree
Showing 20 changed files with 281 additions and 26 deletions.
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

0 comments on commit c2c7cc7

Please sign in to comment.