Skip to content

Commit 2332990

Browse files
Don't follow recursive symlinks during project discovery (#1270)
Project discovery is done by crawling the workspace with `fast-glob` looking for CSS files, JS/TS config files, Sass/Less files, etc… A major problem with this approach though is that `fast-glob` crawls the filesystem and traverses into symlinked directories. If the symlink points back to a parent folder the crawler may cause a handful of issues: - An infinite traversal loop - Out of memory crashes (causing the server to restart repeatedly) - A rather long discovery time (10s+) - Errors because of file path length limits (esp on Windows) I've rewritten the traversal to use `tinyglobby` which is a `fast-glob`-compatible replacement. It avoids all of these issues due to its use of `fdir` which explicitly detects and avoids traversal of recursive symlinks. All existing tests pass as well as an additional test that previously timed out due to the use of recursive symlinks. This PR is going to require extensive testing on Windows with network shares, mapped drives, etc… Fixes #1056 Fixes #1185
1 parent f8313ab commit 2332990

File tree

6 files changed

+72
-35
lines changed

6 files changed

+72
-35
lines changed

packages/tailwindcss-language-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@
6767
"dset": "3.1.4",
6868
"enhanced-resolve": "^5.16.1",
6969
"esbuild": "^0.25.0",
70-
"fast-glob": "3.2.4",
7170
"find-up": "5.0.0",
7271
"jiti": "^2.3.3",
7372
"klona": "2.0.4",
@@ -85,6 +84,7 @@
8584
"stack-trace": "0.0.10",
8685
"tailwindcss": "3.4.17",
8786
"tailwindcss-v4": "npm:[email protected]",
87+
"tinyglobby": "^0.2.12",
8888
"tsconfck": "^3.1.4",
8989
"tsconfig-paths": "^4.2.0",
9090
"typescript": "5.3.3",

packages/tailwindcss-language-server/src/project-locator.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,40 @@ testLocator({
428428
],
429429
})
430430

431+
testLocator({
432+
name: 'Recursive symlinks do not cause infinite traversal loops',
433+
fs: {
434+
'src/a/b/c/index.css': css`
435+
@import 'tailwindcss';
436+
`,
437+
'src/a/b/c/z': symlinkTo('src', 'dir'),
438+
'src/a/b/x': symlinkTo('src', 'dir'),
439+
'src/a/b/y': symlinkTo('src', 'dir'),
440+
'src/a/b/z': symlinkTo('src', 'dir'),
441+
'src/a/x': symlinkTo('src', 'dir'),
442+
443+
'src/b/c/d/z': symlinkTo('src', 'dir'),
444+
'src/b/c/d/index.css': css``,
445+
'src/b/c/x': symlinkTo('src', 'dir'),
446+
'src/b/c/y': symlinkTo('src', 'dir'),
447+
'src/b/c/z': symlinkTo('src', 'dir'),
448+
'src/b/x': symlinkTo('src', 'dir'),
449+
450+
'src/c/d/e/z': symlinkTo('src', 'dir'),
451+
'src/c/d/x': symlinkTo('src', 'dir'),
452+
'src/c/d/y': symlinkTo('src', 'dir'),
453+
'src/c/d/z': symlinkTo('src', 'dir'),
454+
'src/c/x': symlinkTo('src', 'dir'),
455+
},
456+
expected: [
457+
{
458+
version: '4.1.1 (bundled)',
459+
config: '/src/a/b/c/index.css',
460+
content: [],
461+
},
462+
],
463+
})
464+
431465
// ---
432466

433467
function testLocator({

packages/tailwindcss-language-server/src/project-locator.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as os from 'node:os'
21
import * as path from 'node:path'
32
import * as fs from 'node:fs/promises'
4-
import glob from 'fast-glob'
3+
import { glob } from 'tinyglobby'
54
import picomatch from 'picomatch'
65
import type { Settings } from '@tailwindcss/language-service/src/util/state'
76
import { CONFIG_GLOB, CSS_GLOB } from './lib/constants'
@@ -276,14 +275,14 @@ export class ProjectLocator {
276275

277276
private async findConfigs(): Promise<ConfigEntry[]> {
278277
// Look for config files and CSS files
279-
let files = await glob([`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`], {
278+
let files = await glob({
279+
patterns: [`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`],
280280
cwd: this.base,
281281
ignore: this.settings.tailwindCSS.files.exclude,
282282
onlyFiles: true,
283283
absolute: true,
284-
suppressErrors: true,
284+
followSymbolicLinks: true,
285285
dot: true,
286-
concurrency: Math.max(os.cpus().length, 1),
287286
})
288287

289288
let realpaths = await Promise.all(files.map((file) => fs.realpath(file)))

packages/tailwindcss-language-server/tests/prepare.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ import { exec } from 'node:child_process'
22
import * as path from 'node:path'
33
import { fileURLToPath } from 'node:url'
44
import { promisify } from 'node:util'
5-
import glob from 'fast-glob'
5+
import { glob } from 'tinyglobby'
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url))
88

9-
const fixtures = glob.sync(['tests/fixtures/*/package.json', 'tests/fixtures/v4/*/package.json'], {
10-
cwd: path.resolve(__dirname, '..'),
9+
const root = path.resolve(__dirname, '..')
10+
11+
const fixtures = await glob({
12+
cwd: root,
13+
patterns: ['tests/fixtures/*/package.json', 'tests/fixtures/v4/*/package.json'],
14+
absolute: true,
1115
})
1216

1317
const execAsync = promisify(exec)
1418

1519
await Promise.all(
1620
fixtures.map(async (fixture) => {
17-
console.log(`Installing dependencies for ${fixture}`)
21+
console.log(`Installing dependencies for ${path.relative(root, fixture)}`)
1822

1923
await execAsync('npm install', { cwd: path.dirname(fixture) })
2024
}),

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Prerelease
44

55
- Only scan the file system once when needed ([#1287](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1287))
6+
- Don't follow recursive symlinks when searching for projects ([#1270](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1270))
67

78
# 0.14.13
89

pnpm-lock.yaml

Lines changed: 24 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)