Skip to content
Open
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
12 changes: 10 additions & 2 deletions packages/tailwindcss-language-server/src/resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
FileSystem,
} from 'enhanced-resolve'
import { loadPnPApi, type PnpApi } from './pnp'
import { loadTsConfig, type TSConfigApi } from './tsconfig'
import { loadTsConfig, type TSConfigApi, type TSConfigLoadOptions } from './tsconfig'
import { normalizeYarnPnPDriveLetter } from '../utils'

export interface ResolverOptions {
Expand All @@ -34,6 +34,13 @@ export interface ResolverOptions {
*/
tsconfig?: boolean | TSConfigApi

/**
* Glob patterns to exclude while discovering tsconfig files.
*
* This mirrors the semantics of `tailwindCSS.files.exclude`.
*/
tsconfigExclude?: TSConfigLoadOptions['exclude']

/**
* A filesystem to use for resolution. If not provided, the resolver will
* create one and use it internally for itself and any child resolvers that
Expand Down Expand Up @@ -132,7 +139,7 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
tsconfig = opts.tsconfig
} else if (opts.tsconfig) {
try {
tsconfig = await loadTsConfig(opts.root)
tsconfig = await loadTsConfig(opts.root, { exclude: opts.tsconfigExclude })
} catch (err) {
// We don't want to hard crash in case of an error handling tsconfigs
// It does affect what projects we can resolve or how we load files
Expand Down Expand Up @@ -302,6 +309,7 @@ export async function createResolver(opts: ResolverOptions): Promise<Resolver> {
// Inherit defaults from parent
pnp: childOpts.pnp ?? pnpApi,
tsconfig: childOpts.tsconfig ?? tsconfig,
tsconfigExclude: childOpts.tsconfigExclude ?? opts.tsconfigExclude,
fileSystem: childOpts.fileSystem ?? fileSystem,
})
},
Expand Down
42 changes: 42 additions & 0 deletions packages/tailwindcss-language-server/src/resolver/tsconfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as path from 'node:path'
import { expect } from 'vitest'
import { loadTsConfig } from './tsconfig'
import { json, defineTest } from '../testing'
import { normalizePath } from '../utils'

defineTest({
name: 'loadTsConfig respects excluded paths',
fs: {
'tsconfig.json': json`
{
"compilerOptions": {
"baseUrl": "."
}
}
`,
'repos/ai/tsconfig.json': json`
{
"extends": "@vercel/ai-tsconfig/base.json"
}
`,
},
async handle({ root }) {
let withoutExclude = await loadTsConfig(root)
expect(withoutExclude.errors.length).toBeGreaterThan(0)

let rootRelativeExclude = await loadTsConfig(root, {
exclude: ['repos/**'],
})
expect(rootRelativeExclude.errors).toHaveLength(0)

let rootAnchoredExclude = await loadTsConfig(root, {
exclude: ['/repos/**'],
})
expect(rootAnchoredExclude.errors).toHaveLength(0)

let absoluteExclude = await loadTsConfig(root, {
exclude: [normalizePath(path.join(root, 'repos/**'))],
})
expect(absoluteExclude.errors).toHaveLength(0)
},
})
82 changes: 70 additions & 12 deletions packages/tailwindcss-language-server/src/resolver/tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import * as path from 'node:path'
import * as tsconfig from 'tsconfig-paths'
import * as tsconfck from 'tsconfck'
import picomatch from 'picomatch'
import { normalizeDriveLetter, normalizePath } from '../utils'
import { DefaultMap } from '../util/default-map'

Expand Down Expand Up @@ -52,8 +53,15 @@ export interface TSConfigApi {
errors: unknown[]
}

export async function loadTsConfig(root: string): Promise<TSConfigApi> {
let { configs, errors } = await findConfigs(root)
export interface TSConfigLoadOptions {
exclude?: string[]
}

export async function loadTsConfig(
root: string,
options: TSConfigLoadOptions = {},
): Promise<TSConfigApi> {
let { configs, errors } = await findConfigs(root, options.exclude ?? [])

let matchers = await createMatchers(configs)

Expand Down Expand Up @@ -105,7 +113,7 @@ export async function loadTsConfig(root: string): Promise<TSConfigApi> {
}

async function refresh() {
let { configs, errors } = await findConfigs(root)
let { configs, errors } = await findConfigs(root, options.exclude ?? [])

matchers = await createMatchers(configs)

Expand All @@ -123,28 +131,26 @@ export async function loadTsConfig(root: string): Promise<TSConfigApi> {
}
}

async function findConfigs(root: string): Promise<{
async function findConfigs(
root: string,
exclude: string[],
): Promise<{
configs: Set<tsconfck.TSConfckParseResult>
errors: unknown[]
}> {
let isExcluded = createExcludeMatcher(root, exclude)

// 1. Find all tsconfig files in the project
let files = await tsconfck.findAll(root, {
configNames: ['tsconfig.json', 'jsconfig.json'],
skip(dir) {
if (dir === 'node_modules') return true
if (dir === '.git') return true

// TODO: Incorporate thee `exclude` option from VSCode settings.
//
// Doing so here is complicated because we don't have access to the
// full path to the file here and we need that to match it against the
// exclude patterns.
//
// This probably means we need to filter them after we've found them all.

return false
},
})
files = files.filter((file) => !isExcluded(file))

// 2. Load them all
let options: tsconfck.TSConfckParseOptions = {
Expand All @@ -170,6 +176,7 @@ async function findConfigs(root: string): Promise<{

// Mach against referenced projects rather than the project itself
for (let ref of result.referenced) {
if (isExcluded(ref.tsconfigFile)) continue
parsed.add(ref)
}

Expand Down Expand Up @@ -283,3 +290,54 @@ function findBaseDir(project: tsconfck.TSConfckParseResult): string {

return path.dirname(project.tsconfigFile)
}

function createExcludeMatcher(root: string, patterns: string[]) {
if (patterns.length === 0) return (_filepath: string) => false

let normalizedRoot = normalizeDriveLetter(normalizePath(root))

let absoluteMatchers: Array<(filepath: string) => boolean> = []
let relativeMatchers: Array<(filepath: string) => boolean> = []

for (let pattern of patterns) {
pattern = normalizePath(pattern)

let isWorkspaceRootRelativePattern =
pattern.startsWith('/') &&
!pattern.startsWith('//') &&
!pattern.startsWith(`${normalizedRoot}/`) &&
pattern !== normalizedRoot

if (isWorkspaceRootRelativePattern) {
relativeMatchers.push(picomatch(pattern.slice(1), { dot: true }))
continue
}

if (path.isAbsolute(pattern)) {
absoluteMatchers.push(picomatch(normalizeDriveLetter(pattern), { dot: true }))
continue
}

if (pattern.startsWith('/')) {
pattern = pattern.slice(1)
}

relativeMatchers.push(picomatch(pattern, { dot: true }))
}

return (filepath: string) => {
let normalizedFile = normalizeDriveLetter(normalizePath(filepath))

for (let matcher of absoluteMatchers) {
if (matcher(normalizedFile)) return true
}

let relativeFile = normalizePath(path.relative(normalizedRoot, normalizedFile))

for (let matcher of relativeMatchers) {
if (matcher(relativeFile)) return true
}

return false
}
}
1 change: 1 addition & 0 deletions packages/tailwindcss-language-server/src/tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export class TW {
root: base,
pnp: true,
tsconfig: true,
tsconfigExclude: ignore,
})

let locator = new ProjectLocator(base, globalSettings, resolver)
Expand Down