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 9 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
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
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}',
],
},
])
59 changes: 52 additions & 7 deletions packages/tailwindcss-language-server/src/project-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import type { Settings } from '@tailwindcss/language-service/src/util/state'
import { CONFIG_GLOB, CSS_GLOB } from './lib/constants'
import { readCssFile } from './util/css'
import { Graph } from './graph'
import type { Message } from 'postcss'
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 { type Feature, supportedFeatures } from '@tailwindcss/language-service/src/features'
import { resolveCssImports } from './resolve-css-imports'
import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils'
import postcss from 'postcss'

export interface ProjectConfig {
/** The folder that contains the project */
Expand Down Expand Up @@ -341,6 +342,9 @@ export class ProjectLocator {
// Resolve real paths for all the files in the CSS import graph
await Promise.all(imports.map((file) => file.resolveRealpaths()))

// Resolve all @source directives
await Promise.all(imports.map((file) => file.resolveSourceDirectives()))
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

// Create a graph of all the CSS files that might (indirectly) use Tailwind
let graph = new Graph<FileEntry>()

Expand Down Expand Up @@ -500,7 +504,12 @@ async function* contentSelectorsFromCssConfig(entry: ConfigEntry): AsyncIterable
}
} else if (item.kind === 'auto' && !auto) {
auto = true
for await (let pattern of detectContentFiles(entry.packageRoot)) {
let root = entry.entries[0]
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
for await (let pattern of detectContentFiles(
entry.packageRoot,
entry.path,
root?.sources ?? [],
)) {
yield {
pattern,
priority: DocumentSelectorPriority.CONTENT_FILE,
Expand All @@ -510,25 +519,37 @@ async function* contentSelectorsFromCssConfig(entry: ConfigEntry): AsyncIterable
}
}

async function* detectContentFiles(base: string): AsyncIterable<string> {
async function* detectContentFiles(
base: string,
inputFile,
sources: string[],
): AsyncIterable<string> {
try {
let oxidePath = resolveFrom(path.dirname(base), '@tailwindcss/oxide')
oxidePath = pathToFileURL(oxidePath).href

const oxide: typeof import('@tailwindcss/oxide') = await import(oxidePath)
const oxide: typeof import('@tailwindcss/oxide') = await import(oxidePath).then(
(o) => o.default || o,
)
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved

// This isn't a v4 project
if (!oxide.scanDir) return

let { files, globs } = oxide.scanDir({ base, globs: true })
let { files, globs, candidates } = oxide.scanDir({
base,
sources: sources.map((pattern) => ({
base: path.dirname(inputFile),
pattern,
})),
})

for (let file of files) {
yield normalizePath(file)
}

for (let { base, glob } of globs) {
for (let { base, pattern } of globs) {
// Do not normalize the glob itself as it may contain escape sequences
yield normalizePath(base) + '/' + glob
yield normalizePath(base) + '/' + pattern
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
}
} catch {
//
Expand All @@ -553,6 +574,7 @@ class FileEntry {
content: string | null
deps: FileEntry[] = []
realpath: string | null
sources: string[] = []

constructor(
public type: 'js' | 'css',
Expand Down Expand Up @@ -589,6 +611,29 @@ class FileEntry {
await Promise.all(this.deps.map((entry) => entry.resolveRealpaths()))
}

async resolveSourceDirectives() {
if (this.sources.length > 0) {
return
}
// Note: This should eventually use the DesignSystem to extract the same
// sources also discovered by tailwind. Since we don't have everything yet
// to initialize the design system though, we set up a simple postcss at
// rule exporter instead for now.
await postcss([
{
postcssPlugin: 'extract-at-rules',
AtRule: {
source: ({ params }: AtRule) => {
if (params[0] !== '"' && params[0] !== "'") {
return
}
this.sources.push(params.slice(1, -1))
},
},
},
]).process(this.content, { from: this.realpath })
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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
Expand Down
124 changes: 124 additions & 0 deletions packages/tailwindcss-language-server/src/resolve-css-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import postcss from 'postcss'
import postcssImport from 'postcss-import'
import { createResolver } from './util/resolve'

import path from 'node:path'
import type { AtRule, Plugin } from 'postcss'

const resolver = createResolver({
extensions: ['.css'],
mainFields: ['style'],
Expand All @@ -15,8 +18,129 @@ const resolveImports = postcss([
return paths ? paths : id
},
}),
fixRelativePathsPlugin(),
])

export function resolveCssImports() {
return resolveImports
}

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

export default function fixRelativePathsPlugin(): Plugin {
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
// Retain a list of touched at-rules to avoid infinite loops
let touched: WeakSet<AtRule> = new WeakSet()

function fixRelativePath(atRule: AtRule) {
let rootPath = atRule.root().source?.input.file
if (!rootPath) {
return
}

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

if (touched.has(atRule)) {
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,
},
}
}

// Inlined version of `normalize-path` <https://github.com/jonschlinkert/normalize-path>
// Copyright (c) 2014-2018, Jon Schlinkert.
// Released under the MIT License.
function normalizePathBase(path: string, stripTrailing?: boolean) {
thecrypticace marked this conversation as resolved.
Show resolved Hide resolved
if (typeof path !== 'string') {
throw new TypeError('expected path to be a string')
}

if (path === '\\' || path === '/') return '/'

var len = path.length
if (len <= 1) return path

// ensure that win32 namespaces has two leading slashes, so that the path is
// handled properly by the win32 version of path.parse() after being normalized
// https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces
var prefix = ''
if (len > 4 && path[3] === '\\') {
var ch = path[2]
if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') {
path = path.slice(2)
prefix = '//'
}
}

var segs = path.split(/[/\\]+/)
if (stripTrailing !== false && segs[segs.length - 1] === '') {
segs.pop()
}
return prefix + segs.join('/')
}

export function normalizePath(originalPath: string) {
let normalized = normalizePathBase(originalPath)

// Make sure Windows network share paths are normalized properly
// They have to begin with two slashes or they won't resolve correctly
if (
originalPath.startsWith('\\\\') &&
normalized.startsWith('/') &&
!normalized.startsWith('//')
) {
return `/${normalized}`
}

return normalized
}
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ withFixture('v4/basic', (c) => {
let result = await completion({ lang, text, position, settings })
let textEdit = expect.objectContaining({ range: { start: position, end: position } })

expect(result.items.length).toBe(12376)
expect(result.items.filter((item) => item.label.endsWith(':')).length).toBe(220)
expect(result.items.length).toBe(12400)
expect(result.items.filter((item) => item.label.endsWith(':')).length).toBe(224)
expect(result).toEqual({
isIncomplete: false,
items: expect.arrayContaining([
Expand Down
Loading