Skip to content

Commit

Permalink
Add support for @source (#1030)
Browse files Browse the repository at this point in the history
This PR does multiple things:

1. It adds support for the `@plugin` and `@source` directive in the CSS
language.
2. We will now scan V4 config files for eventually defined `@source`
rules and respect them similarly how we handled custom content paths in
V3.
3. Add support for the the Oxide API coming in Alpha 20
(tailwindlabs/tailwindcss#14187)

For detecting the right content, we load the Oxide API installed in the
user's Tailwind project. To do this in a backward compatible way, we now
also load the `package.json` file of the installed Oxide version and
support previous alpha releases for a limited amount of time.

---------

Co-authored-by: Jordan Pittman <[email protected]>
  • Loading branch information
philipp-spiess and thecrypticace authored Aug 16, 2024
1 parent 3f79be2 commit 3fc767b
Show file tree
Hide file tree
Showing 31 changed files with 582 additions and 130 deletions.
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@tailwindcss/forms": "0.5.3",
"@tailwindcss/language-service": "workspace:*",
"@tailwindcss/line-clamp": "0.4.2",
"@tailwindcss/oxide": "^4.0.0-alpha.16",
"@tailwindcss/oxide": "^4.0.0-alpha.19",
"@tailwindcss/typography": "0.5.7",
"@types/color-name": "^1.1.3",
"@types/culori": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Plugin } from 'postcss'

export function extractSourceDirectives(sources: string[]): Plugin {
return {
postcssPlugin: 'extract-at-rules',
AtRule: {
source: ({ params }) => {
if (params[0] !== '"' && params[0] !== "'") return
sources.push(params.slice(1, -1))
},
},
}
}
69 changes: 69 additions & 0 deletions packages/tailwindcss-language-server/src/css/fix-relative-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import path from 'node:path'
import type { AtRule, Plugin } from 'postcss'
import { normalizePath } from '../utils'

const SINGLE_QUOTE = "'"
const DOUBLE_QUOTE = '"'

export function fixRelativePaths(): Plugin {
// Retain a list of touched at-rules to avoid infinite loops
let touched: WeakSet<AtRule> = new WeakSet()

function fixRelativePath(atRule: AtRule) {
if (touched.has(atRule)) return

let rootPath = atRule.root().source?.input.file
if (!rootPath) return

let inputFilePath = atRule.source?.input.file
if (!inputFilePath) return

let value = atRule.params[0]

let quote =
value[0] === DOUBLE_QUOTE && value[value.length - 1] === DOUBLE_QUOTE
? DOUBLE_QUOTE
: value[0] === SINGLE_QUOTE && value[value.length - 1] === SINGLE_QUOTE
? SINGLE_QUOTE
: null

if (!quote) return

let glob = atRule.params.slice(1, -1)

// Handle eventual negative rules. We only support one level of negation.
let negativePrefix = ''
if (glob.startsWith('!')) {
glob = glob.slice(1)
negativePrefix = '!'
}

// We only want to rewrite relative paths.
if (!glob.startsWith('./') && !glob.startsWith('../')) {
return
}

let absoluteGlob = path.posix.join(normalizePath(path.dirname(inputFilePath)), glob)
let absoluteRootPosixPath = path.posix.dirname(normalizePath(rootPath))

let relative = path.posix.relative(absoluteRootPosixPath, absoluteGlob)

// If the path points to a file in the same directory, `path.relative` will
// remove the leading `./` and we need to add it back in order to still
// consider the path relative
if (!relative.startsWith('.')) {
relative = './' + relative
}

atRule.params = quote + negativePrefix + relative + quote
touched.add(atRule)
}

return {
postcssPlugin: 'tailwindcss-postcss-fix-relative-paths',
AtRule: {
source: fixRelativePath,
plugin: fixRelativePath,
},
}
}
2 changes: 2 additions & 0 deletions packages/tailwindcss-language-server/src/css/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './resolve-css-imports'
export * from './extract-source-directives'
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import postcss from 'postcss'
import postcssImport from 'postcss-import'
import { createResolver } from './util/resolve'
import { createResolver } from '../util/resolve'
import { fixRelativePaths } from './fix-relative-paths'

const resolver = createResolver({
extensions: ['.css'],
Expand All @@ -15,6 +16,7 @@ const resolveImports = postcss([
return paths ? paths : id
},
}),
fixRelativePaths(),
])

export function resolveCssImports() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ async function validateTextDocument(textDocument: TextDocument): Promise<void> {
.filter((diagnostic) => {
if (
diagnostic.code === 'unknownAtRules' &&
/Unknown at rule @(tailwind|apply|config|theme)/.test(diagnostic.message)
/Unknown at rule @(tailwind|apply|config|theme|plugin|source)/.test(diagnostic.message)
) {
return false
}
Expand Down
143 changes: 143 additions & 0 deletions packages/tailwindcss-language-server/src/oxide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 {
interface GlobEntry {
base: string
glob: string
}

interface ScanOptions {
base: string
globs?: boolean
}

interface ScanResult {
files: Array<string>
globs: Array<GlobEntry>
}
}

// This covers the Oxide API from v4.0.0-alpha.19
declare namespace OxideV2 {
interface GlobEntry {
base: string
pattern: string
}

interface ScanOptions {
base: string
sources: Array<GlobEntry>
}

interface ScanResult {
files: Array<string>
globs: Array<GlobEntry>
}
}

// This covers the Oxide API from v4.0.0-alpha.20+
declare namespace OxideV3 {
interface GlobEntry {
base: string
pattern: string
}

interface ScannerOptions {
detectSources?: { base: string }
sources: Array<GlobEntry>
}

interface ScannerConstructor {
new (options: ScannerOptions): Scanner
}

interface Scanner {
files: Array<string>
globs: Array<GlobEntry>
}
}

interface Oxide {
scanDir?(options: OxideV1.ScanOptions): OxideV1.ScanResult
scanDir?(options: OxideV2.ScanOptions): OxideV2.ScanResult
Scanner?: OxideV3.ScannerConstructor
}

async function loadOxideAtPath(id: string): Promise<Oxide | null> {
let oxide = await import(id)

// This is a much older, unsupported version of Oxide before v4.0.0-alpha.1
if (!oxide.scanDir) return null

return oxide
}

interface GlobEntry {
base: string
pattern: string
}

interface ScanOptions {
oxidePath: string
oxideVersion: string
basePath: string
sources: Array<GlobEntry>
}

interface ScanResult {
files: Array<string>
globs: Array<GlobEntry>
}

/**
* This is a helper function that leverages the Oxide API to scan a directory
* and a set of sources and turn them into files and globs.
*
* Because the Oxide API has changed over time this function presents a unified
* interface that works with all versions of the Oxide API but the results may
* be different depending on the version of Oxide that is being used.
*
* For example, the `sources` option is ignored before v4.0.0-alpha.19.
*/
export async function scan(options: ScanOptions): Promise<ScanResult | null> {
const oxide = await loadOxideAtPath(options.oxidePath)
if (!oxide) return null

// V1
if (lte(options.oxideVersion, '4.0.0-alpha.18')) {
let result = oxide.scanDir?.({
base: options.basePath,
globs: true,
})

return {
files: result.files,
globs: result.globs.map((g) => ({ base: g.base, pattern: g.glob })),
}
}

// V2
if (lte(options.oxideVersion, '4.0.0-alpha.19')) {
let result = oxide.scanDir({
base: options.basePath,
sources: options.sources,
})

return {
files: result.files,
globs: result.globs,
}
}

// V3
let scanner = new oxide.Scanner({
detectSources: { base: options.basePath },
sources: options.sources,
})

return {
files: scanner.files,
globs: scanner.globs,
}
}
26 changes: 26 additions & 0 deletions packages/tailwindcss-language-server/src/project-locator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,29 @@ testFixture('v4/auto-content', [
],
},
])

testFixture('v4/custom-source', [
//
{
config: 'admin/app.css',
content: [
'{URL}/admin/**/*.{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}/admin/**/*.bin',
'{URL}/admin/foo.bin',
'{URL}/package.json',
'{URL}/shared.html',
'{URL}/web/**/*.{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}',
],
},
{
config: 'web/app.css',
content: [
'{URL}/admin/**/*.{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}/web/*.bin',
'{URL}/web/bar.bin',
'{URL}/package.json',
'{URL}/shared.html',
'{URL}/web/**/*.{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}',
],
},
])
Loading

0 comments on commit 3fc767b

Please sign in to comment.