Skip to content
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

Add support for @source #1030

Merged
merged 22 commits into from
Aug 16, 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/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