Tailwind CSS 的 globs 为何会扫描被 gitignore 的目录

👁 78 次阅读

Tailwind Scanner globs 为何仍遍历 gitignore 目录

来源:https://github.com/tailwindlabs/tailwindcss/issues/20533

现象描述

issue #20533 报告的是 @tailwindcss/oxide 中 Scanner 的一处不一致行为:自动源检测在构建 scanner.files 时会正确遵守 .gitignore,但在计算 scanner.globs 时却会走进被 gitignore 忽略的目录。返回的 globs 结果是正确的(被忽略的目录并不包含在内),问题纯粹出在遍历过程本身。 报告者给出的复现版本是 @tailwindcss/oxide 4.3.3,并说明在 4.1.14 中表现相同。构建工具使用 @tailwindcss/vite,同时也在 JetBrains Tailwind CSS 插件(其 oxide-helper.js)以及直接调用 Scanner API 时复现。环境为 Node.js v24.18.1,操作系统 Linux(btrfs)。 报告者提供了一个自包含的复现脚本,无需完整项目:
// repro.mjs — run: strace -f -e trace=statx,openat -o trace.log node repro.mjs && grep -c 'repo/var/cache' trace.log
import { createRequire } from 'node:module';
import { mkdirSync, writeFileSync } from 'node:fs';
import { execSync } from 'node:child_process';

const require = createRequire(import.meta.url);
const { Scanner } = require('@tailwindcss/oxide');

const root = '/tmp/oxide-repro/repo';
mkdirSync(${root}/var/cache/profiler/aa, { recursive: true });
mkdirSync(${root}/templates, { recursive: true });
writeFileSync(${root}/.gitignore, '/var/\n');
writeFileSync(${root}/templates/page.html, '<div class="flex"></div>');
writeFileSync(${root}/var/cache/profiler/aa/bb1234, '<div class="grid"></div>');
execSync('git init -q', { cwd: root });

const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*', negated: false }] });
console.log('files:', scanner.files);  // correct: only templates/page.html
console.log('globs:', scanner.globs);  // correct result, but computing it walks var/
从 strace 输出可以观察到两条路径的差异:
  • files 遍历会打开 var/,检查 var/.gitignore、var/.ignore 与 var/.git/info/exclude,然后停止,行为正确。
  • globs 遍历则会依次打开 var/、var/cache、var/cache/profiler、var/cache/profiler/aa,并对其中每个文件执行 stat,整个过程中没有读取任何 ignore 文件。

背景原因

在大型被忽略目录存在时,这种多余的遍历代价很高。报告者描述其项目里有一个 Symfony 的 var/cache/dev/profiler 目录,包含约 1300 万个小文件:
  • vite build 在 read_extent_buffer_pages 处挂起超过 20 分钟,单线程阻塞在主线程上,位置正是读取 globs。
  • JetBrains Tailwind helper 每个进程持续占用 15%–25% CPU。该 helper 总会附加 { base: <project root>, pattern: '**/*' },因此无论 CSS 内容如何,这次遍历都会发生。
据 issue 讨论,该行为在 main 分支上已不再复现。参与者 akasakariko 表示已对此进行调查,在 v4.3.3 上复现了报告描述的 globs 遍历进入被忽略的 var/cache 子目录的行为,但在当前 main 上同样的测试不再进入这些目录。该参与者将修复归因于 PR #20408(标题为 “Fix slow Vite rebuilds in projects with large gitignored directories”,合并于 2026-08-12),并称 scanner 的 71 个集成测试在 main 上全部通过。 这里需要区分两个说法:代码合并进主干与修复随某个已发布版本分发,是两件不同的事。截至 issue 讨论,可核实的信息是 PR #20408 已被合并到 main,而包含该修复的稳定版本号在讨论中并未给出。

当前处理路径与局限

该 issue 目前为 open 状态,官方尚未在本 issue 下给出实质回应与修复版本方案。讨论中的实质内容来自社区侧的三条评论,可归纳为以下两点:
  • 代码已进主干,但未进入已发布二进制。 据 issue 讨论,akasakariko 称在 v4.3.3 上复现了问题,在 main 上不再复现,并指向已合并的 PR #20408。这意味着使用版本化二进制分发的工具(IDE 插件等)仍会遇到该行为。
  • 发布渠道需要补丁版本。 FluffyDiscord 的回应是,如果该说法成立,需要一个 hotfix release,让 IDE 和其他使用版本化二进制的工具能够拿到,并强调这不是小问题:它可以让项目构建变得极慢,在 dev build 中甚至可能无法完成构建。
由于官方尚无实质回复,本节只归纳讨论中已经出现的动作与建议,不补写额外的排查步骤或命令。

参数或对比表格

项目files 遍历globs 遍历
是否读取 ignore 文件是(.gitignore、.ignore、.git/info/exclude)否
是否进入 gitignore 目录否是(在受影响版本上)
返回结果正确性正确正确,仅遍历过程有问题
受影响版本—v4.3.3、4.1.14(据 issue 讨论)
主干状态—main 上不再复现(据 issue 讨论,关联 PR #20408)

常见错误排查

1. 看到构建长时间卡住且 CPU 单核占满。 报告者的观测是 vite build 在 read_extent_buffer_pages 处挂起超过 20 分钟,单线程阻塞在主线程上,位置在读取 globs。这属于可观察现象,具体成因需结合本地项目结构判断。 2. IDE 插件进程持续占用 CPU。 报告者观测到 JetBrains Tailwind helper 每个进程 15%–25% CPU,并指出该 helper 总会附加 { base: <project root>, pattern: '**/*' },因此遍历与 CSS 内容无关。 3. 怀疑 globs 结果本身有误。 报告者明确说明返回的 globs 是正确的,被忽略的目录不在其中,问题仅在遍历过程。排查时不应把方向放在结果错误上。 4. 在 4.3.3 上确认是否仍存在。 据 issue 讨论,该行为在 v4.3.3 可复现;若使用版本化二进制,需要确认所用工具实际加载的 oxide 版本。 5. 在 main 上验证是否已消失。 据 issue 讨论,当前 main 上同样的测试不再进入被忽略目录,scanner 的 71 个集成测试全部通过。但 main 上的状态不等同于已发布版本的状态。

完整解决方案(含代码)

截至 issue 讨论,本 issue 处于 open 状态,官方尚未在本条目下合入方案或给出修复版本号。据 issue 讨论,修复已通过 PR #20408 进入 main,但该修复尚未反映到 v4.3.3 这类已发布版本中。在官方发布包含该修复的版本之前,可行的处理路径如下。 使用 main 之外的渠道获取修复。 由于修复已进主干但未进入版本化二进制,依赖已发布包的工具无法直接受益。是否需要切换分支或等待 hotfix release,取决于使用方对稳定性的要求。 核实所依赖工具的 oxide 版本。 报告者复现的版本为 4.3.3,并说明 4.1.14 表现相同。对于 IDE 插件,需要确认其内置或加载的 oxide 二进制版本。 复现脚本可用于本地确认。 报告者提供的 repro.mjs 不依赖完整项目,可直接用于判断本地环境是否触发 globs 遍历进入被忽略目录。脚本中的配置为 { sources: [{ base: root, pattern: '**/*', negated: false }] },通过对比 scanner.files 与 scanner.globs 的输出以及 strace 中是否出现 repo/var/cache 计数来判断。 在项目侧减少被忽略目录的文件数量。 报告者描述的痛点是 var/cache/dev/profiler 下约 1300 万个小文件。在修复版本可用之前,控制此类目录的规模是缓解遍历代价的直接手段。该方向属于应用层缓解,不改变 scanner 行为本身。

实践建议

  • 在受影响版本上,避免在项目根目录放置包含大量文件的被忽略目录,尤其是缓存与 profiler 类目录。
  • 对使用 @tailwindcss/vite 或其他封装构建工具的项目,构建卡顿需要同时排查 oxide 的 files 与 globs 两条遍历路径,而不是只看 CSS 配置。
  • 对 IDE 插件,注意其可能无条件附加 **/* 模式,因此即使项目 CSS 未显式声明扫描范围,遍历仍可能发生。
  • 关注 PR #20408 的发布状态,并通过 GitHub API 查询 PR 状态以核实其是否已进入目标版本,而不是仅依据 “已在 main 修复” 的说法。
  • 若在较新版本上仍复现,按 issue 讨论的惯例,应记录复现的版本号并向对应渠道上报。

FAQ

问:这个问题的根因是什么? 答:自动源检测在构建 scanner.files 时遵守 .gitignore,但在计算 scanner.globs 时不读取任何 ignore 文件,直接走进被忽略目录。返回的 globs 结果本身正确,问题在遍历过程。 问:哪些版本受到影响? 答:据 issue 讨论,复现版本为 @tailwindcss/oxide 4.3.3,4.1.14 表现相同。 问:main 分支上是否已修复? 答:据 issue 讨论,akasakariko 称在 main 上同样测试不再进入被忽略目录,并归因于已合并的 PR #20408,scanner 的 71 个集成测试在 main 上全部通过。该说法属于社区参与者的调查结论。 问:是否已经有包含修复的正式版本? 答:截至 issue 讨论,没有给出包含该修复的稳定版本号。讨论中有参与者提出需要 hotfix release,以便使用版本化二进制的工具能够获取。 问:在修复版本发布前可以做什么? 答:报告者提供的复现脚本可用于本地确认是否触发该遍历;同时在项目侧控制被忽略目录的文件规模,尤其是缓存与 profiler 类目录,可以缓解遍历带来的代价。

订阅 AI 百事知

每周精选 AI 工具评测、行业资讯与使用教程,第一时间送达。

🔗 相关阅读(你可能还想看)

📄 Outlook 节点创建草稿添加附件为何报 UnableToDeserializePostBody?
📄 Flowise 点击市场或聊天页为何整页白屏?
📄 如何解决 Azure AI Search 向量存储的 $filter 递归深度超限错误?
📄 如何解决 shadcn/ui Sidebar 与 Header 重叠的问题?
📄 Next.js 开发服务器为何会 JavaScript heap out of memory?
📄 Nuxt 偶发 Cannot read properties of undefined 报错是怎么回事
📄 如何解决「Visual Studio Code for ipad」相关的问题?
📄 如何在AutoGen中实现企业级代理治理的加密行动收据(AAR)?
📄 记忆内容未清洗直接注入系统提示词,如何防范间接提示注入?
crewAI LiteAgent 将记忆内容直接拼接到系统提示词,存在间接提示注入风险。社区提出分层防御方案,但 iss
🤖 想直接和 AI 对话?
免费体验 DeepSeek V4 等顶级模型,无需注册,随问随答
🎯 立即体验 AI 工具

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

🤖 嘟嘟客服
滚动至顶部
🤖 嘟嘟客服
🤖 嘟嘟客服
';i.value='';m.innerHTML+='
...
';fetch('/dify-chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,user:'w'+Date.now()})}).then(function(r){return r.json()}).then(function(d){var a=d.answer||'暂时无法回复';m.removeChild(m.lastChild);m.innerHTML+='
'+a+'
'}).catch(function(){m.removeChild(m.lastChild);m.innerHTML+='
网络异常
'})">发送