Panda CSS v2 Rust 引擎的 crate 分层架构:crates 工作区的一向依赖设计深度解析
Panda CSS v2 Rust 引擎的 crate 分层架构:crates 工作区的一向依赖设计深度解析
Panda CSS(panda 仓库)的 v2 引擎在 crates/ 工作区中用 Rust 重写了提取、编码、样式表生成与代码生成等核心链路。本文以 design-notes/crate-layering.md 为骨架,结合各 crate 的 Cargo.toml 与源码,系统讲解这套六层(Tier 0~Tier 5)的分层依赖设计:每一层的职责边界、代表性 crate 的内部结构、以及"新功能应该放哪一层"的决策方法。读完本文,你将能读懂该仓库的依赖拓扑,理解为什么"extract → encode → emit"的单向管道必须体现在依赖层面,也能在向该引擎贡献代码时快速定位功能归属。
为什么 crates 工作区需要分层,而不是扁平堆放
crates/ 工作区不是一个"扁平的 crate 袋子"(flat bag of crates),而是一条分层的依赖线(tiered dependency line)。其核心约束是:依赖只朝一个方向流动——
- 基础设施(infrastructure)crate 不知道任何解析逻辑;
- 解析(parsing)crate 不知道任何遍历(traversal)逻辑;
- 遍历 crate 不知道任何编排(orchestration)逻辑。
这种形状有两个直接收益:其一,重构时只要保持依赖方向不变,正确性就是可保持的(refactors correctness-preserving);其二,它阻止后来的贡献者无意中把叶子 crate(leaf crate)耦合到 walker 机制上。crates/RUST_GUIDE.md 中的审查清单也把"跨 tier 向上 import"列为禁止项("Never import upward across crate tiers"),可见这一约束既是架构文档的规定,也是代码评审时的硬性检查点。
整个工作区由根目录 Cargo.toml 声明,members 包含 bench、crates/* 以及 packages/compiler/crate、packages/compiler-wasm/crate 两个原生绑定 crate;workspace 统一指定 edition = "2024"、rust-version = "1.93",并对 release 构建使用 codegen-units = 1、lto = "thin"、panic = "abort" 等热路径友好的 profile 设置。
Tier 0 —— 基础设施层
Tier 0 是五个不依赖任何 Panda 域逻辑的 crate:pandacss_fs、pandacss_shared、pandacss_literal、pandacss_tracing、pandacss_sfc。
pandacss_fs:文件系统抽象,让同一份代码能编译到 WASM
pandacss_fs 是文件系统抽象层。其核心是 file_system.rs 中定义的 FileSystem trait:它继承了 oxc_resolver::FileSystem 的读取原语(read、read_to_string、metadata、canonicalize 等),并新增 write、write_if_changed、read_dir、exists 和 glob。关键设计是:核心 crate 只依赖 FileSystem trait,从不直接触碰 std::fs,因此同一套代码可以编译到 wasm32-unknown-unknown。
从 Cargo.toml 可以看到 feature 门控的实现方式:
[features]
default = ["os"]
os = ["dep:walkdir"]
memory = []
默认启用 os(引入 walkdir),而 memory feature 提供内存文件系统实现,供测试使用。其 glob 能力依赖 fast-glob,声明在 workspace.dependencies 中(fast-glob = "1")。完整的 glob 行为(globstar 匹配、零段匹配、嵌套深度等)在 tests/glob.rs 中有大量用例覆盖。详见 design-notes/filesystem.md。
pandacss_shared:无依赖工具集 + CSS 属性名的单一事实来源
pandacss_shared 持有零 Panda 依赖的辅助函数,其中最重要的一项是 CSS 属性名列表的唯一事实来源:css_properties.rs 中的 CSS_PROPERTY_NAMES 常量(文档标注 @generated,来自 mdn-data),以及 is_css_property 成员判断函数。
它的下游消费方横跨两个方向:
- 提取器用
is_css_property做成员判断——判断一个属性是否"合法可提取"; - 代码生成器用同一份列表生成
CssProperties接口成员——决定"类型里提供哪些属性"。
因为两边读的是同一份数据,"能提取的"和"类型里给出的"永远不会分叉(can't diverge)。其 Cargo.toml 只依赖 itoa、regex、rustc-hash、ryu、serde、serde_json,没有引入任何 pandacss_* 依赖,印证了"dependency-free helpers"的定位。
pandacss_literal:宿主无关的提取值树(IR)
pandacss_literal 拥有解析、recipes、编码、utility 元数据以及项目变换共用的宿主无关提取值树(host-neutral extracted value tree,即 Literal 值模型)。把它放在进程类 crate(process crates)之下,可以防止叶子数据 crate 因为传递依赖而拉进 Oxc 解析机制。
这一点在 Cargo.toml 中一目了然:它只依赖 pandacss_shared、serde、serde_json,没有任何 Oxc 依赖。
pandacss_sfc:把框架容器变成等字节偏移的 JS 程序
pandacss_sfc 负责把框架容器(Astro、Vue、Svelte)在相同字节偏移下转换成一个 JS 程序,且共享同一个 JS lexer。文档强调:Astro 的 token 化方式与 Astro 自身 parser 一致;该 crate 没有依赖、没有 Oxc,提取器负责解析它的输出。
其 Cargo.toml 印证了这一点:<a href="https://link.gitcode.com/i/e8a1c89afa490afb8894878be9371a52" target="_blank">dependencies] 一节为空,Oxc 相关 crate 只出现在 [dev-dependencies](oxc_ast、oxc_parser、oxc_span 等仅用于测试适配)。详见 [design-notes/astro-parser.md。
pandacss_tracing:可选的性能追踪工具
pandacss_tracing 依赖 tracing、tracing-chrome、tracing-subscriber 与 serde_json(见 Cargo.toml),为编译流水线提供 chrome 格式的性能追踪能力,属于纯基础设施。
Tier 1 —— 叶子数据与解析层
Tier 1 是 pandacss_config、pandacss_tokens、pandacss_recipes 三个 crate:纯数据模型,只负责从可序列化配置或 pandacss_literal::Literal 解析出类型化形状,不涉及遍历、编码或 I/O。
pandacss_config定义UserConfig,是规范化的已解析配置输入,被 project/system 构造阶段消费。其 Cargo.toml 只依赖indexmap、pandacss_shared、serde、serde_json。pandacss_tokens承载 token 字典(TokenDictionary,见 system.rs 的引用),并提供一个可选serdefeature(见 Cargo.toml)。pandacss_recipes定义Recipe/SlotRecipe类型,dev-dependency 中使用pandacss_extractor做测试接线(见 Cargo.toml),本身则依赖pandacss_literal与pandacss_shared。
从依赖矩阵看,这三个 crate 均未引入 Oxc 或 smallvec 等遍历/分配机制,这正是"Tier-1 消费者不应该传递性地拉进 walker 机制"这一决策的落点。
Tier 2 —— 进程层
Tier 2 是 pandacss_extractor、pandacss_encoder、pandacss_utility、pandacss_stylesheet、pandacss_codegen 五个 crate,承担提取、编码、发射的实际计算工作。
pandacss_extractor:经 Oxc 解析源文件,产出 Literal 与调用记录
pandacss_extractor 通过 Oxc 解析源码,产出 Literal 值以及 ExtractedCall / ExtractedJsx 记录。其 Cargo.toml 引入了完整的 Oxc 工具链(oxc_allocator、oxc_ast、oxc_parser、oxc_semantic、oxc_resolver、oxc_syntax 等),同时依赖 pandacss_sfc、pandacss_fs、pandacss_literal、pandacss_shared 与 pandacss_tokens——后者正是"静态求值器可以折叠 token('colors.red.500') 调用"的实现基础。它的 os / memory features 会透传给 pandacss_fs。
pandacss_encoder:消费 Tier 1 类型,产出原子 Atom
pandacss_encoder 消费 Tier 1 的 Recipe、SlotRecipe、Literal,产出原子的 Atom 记录。文档特别说明:encoder 与 extractor 是兄弟 tier——它们处于不同的工作轴,互不依赖。从 Cargo.toml 看,它确实不依赖 pandacss_extractor(后者仅作为 dev-dependency 用于测试)。
encoder 内部有两个关键入口(见 lib.rs):
process_atomic(约 468 行):遍历 style 对象,每个叶子发射一个 atom,镜像 JS 编码器的processAtomic;process_atomic_with(约 480 行):融合版本——单次遍历中内联应用norm(key 解析、叶子归一化、响应式数组展开),跳过前置的StyleNormalizer.normalize分配遍。这就是"融合 normalize+walk 两遍"的实现。
融合能力来自它定义的 NormalizeAtomic trait(约 363 行),默认实现全是 no-op,由项目层注入 pandacss_utility::StyleNormalizer。
pandacss_utility:配置派生的 utility 元数据与 StyleNormalizer
pandacss_utility 持有配置派生的 utility 元数据(shorthands 简写、值别名、按属性的 layer 覆盖)以及 StyleNormalizer。它的 Cargo.toml 依赖 pandacss_encoder,因为它要实现 NormalizeAtomic trait——这是文档中唯一一个记录的兄弟 Tier-2 依赖,服务于 normalize+walk 两遍的融合。
StyleNormalizer 定义在 normalize.rs(约 17 行),携带 utility、breakpoints 与 ShorthandPolicy(UserFacing / Internal 两种策略,约 10 行),并提供 user_facing / internal 两个构造器;impl pandacss_encoder::NormalizeAtomic for StyleNormalizer 位于同文件约 144 行。
pandacss_stylesheet:原子 → CSS 字符串的发射器/压缩写入器
pandacss_stylesheet 消费编码后的 atoms、recipe 快照、utility 元数据以及受支持的静态 CSS 配置子集,产出 CSS 字符串。它依赖 pandacss_encoder 获取 snapshot/atom 类型,但不依赖 pandacss_project——project 只是 dev-dependency,仅用于测试接线(见 Cargo.toml 的 [dev-dependencies]:pandacss_project、pandacss_system)。
文档强调它的角色边界:它是 emitter/minifying writer,不是 CSS 优化器(canonical boundary 见 design-notes/stylesheet.md)。从源码结构看,它内部按职责拆成 emitter、writer、selector、sort、conditions、preflight、static_css 等模块,与"发射 + 压缩写入"的定位一致。
pandacss_codegen:从配置与 tokens 渲染生成产物
pandacss_codegen 渲染生成的 styled-system 产物(类型声明、helpers 等)。它依赖 pandacss_stylesheet 只有一个目的:theme CSS 条目——这样 theme 文件与样式表共享同一个 emitter。其 Cargo.toml 同时引入了 oxc_codegen、oxc_isolated_declarations 等用于类型/声明生成的 Oxc 工具,以及 pandacss_config、pandacss_shared、pandacss_tokens。
Tier 3 —— 已编译配置层
Tier 3 只有 pandacss_system 一个 crate。System 把 pandacss_config::UserConfig 编译一次成只读的运行时模型,包含:
- 提取器匹配器(extractor matchers);
- utility 元数据;
- conditions 条件;
- recipe 与 pattern 注册表;
- 提取与变换共享的 style-encoding 与 class-name 辅助函数。
从 system.rs 的结构体字段可以确认这些内容:extractor_config、utility、conditions、breakpoints、patterns: PatternRegistry、recipes: RecipeRegistry、config_recipes、config_slot_recipes、keyframes、view_transitions、position_try、optimize、hash_class_names、config_fingerprint、diagnostics 等(约 35 行起)。SystemInput(约 57 行)接受 UserConfig 并可附带外部 token 字典与诊断,System::new(约 77 行)执行配置编译。
设计要点:System 不持有任何构建或 watch 状态;唯一的 setup 期变更就是挂接一个跨文件解析器(cross-file resolver)。引用计数的 recipe 缓存类型也住在这里,与它读取的注册表同处,但其实例归属 Project。
pandacss_system 的 Cargo.toml 依赖 pandacss_config、pandacss_encoder、pandacss_extractor、pandacss_literal、pandacss_recipes、pandacss_shared、pandacss_tokens、pandacss_utility——注意它不依赖 pandacss_project,与"编译后的只读模型与可变项目状态分离"的定位相符。
Tier 4 —— 项目状态与源码重写层
Tier 4 是 pandacss_project 与 pandacss_transform。
pandacss_project:包装 Arc<System>,持有可变构建/watch 状态
Project 包装一个 Arc<System>,并拥有可变的构建与 watch 状态:已解析文件、引用计数的 atom 与 recipe 缓存、依赖图、build info。从 lib.rs 的模块划分(dependency_graph、build_info、parsed_file、diagnostics)可以确认这些职责。
关键约束:它独立于 CSS 渲染——对外暴露借用式的样式表快照(borrowed stylesheet snapshots),但不依赖 pandacss_stylesheet。其 Cargo.toml 的依赖列表里确实没有 pandacss_stylesheet,印证了这一边界。文档开头的示例展示了它的用法:
use pandacss_config::UserConfig;
use pandacss_project::Project;
use pandacss_system::System;
let config = UserConfig::default();
let system = System::new(config)?;
let mut project = Project::new(system);
project.parse_file("button.tsx", "import { css } from '@panda/css'; css({ color: 'red' });");
// …
let atoms = project.atoms();
let recipes = project.recipes();
let summary = project.summary();
Project::atoms 恒返回所有已知文件的并集,因此移除或替换文件不会留下幽灵 atom。项目生命周期细节见 design-notes/project-lifecycle.md。
pandacss_transform:只读 &System,返回新的源字符串
pandacss_transform 只依赖 pandacss_system(project 仅为 dev-dependency,用于测试接线),它从 &System 重写源码,因此宿主可以在没有项目状态的情况下做变换。入口函数位于 lib.rs:
transform_source(约 43 行):用空ParseTransforms重写一个源文件;transform_source_with(约 55 行):应用与Project::parse_file_with相同的回调包(尤其source+pattern)。
两者都接收只读 &System、路径与源码文本,返回新的字符串(外加 source map,dev-dependency 中的 oxc_sourcemap 印证了这一点)。它通过 extract_transform_with_recipes 复用提取器的变换事实收集,并与 parse_file_with 共享 ParseTransforms 与 Config 的 class-name 辅助函数,但绝不触碰项目构建/watch 状态。
Tier 5 —— 编译编排层
Tier 5 只有 pandacss_compiler:它是宿主无关的应用层(host-neutral application layer)。从 lib.rs 的模块导出来看,它包含:
css:组合项目快照与样式表发射(compile_css、compile_split_css、compile_keyframes等);codegen:渲染生成产物(generate_artifact、generate_artifacts、generate_affected_artifacts);views:编译面向的配置与工具视图;setup::load_system:加载并校验系统;inspect_file_source:分类一个文件的 Panda 用法,供 lint 与 IDE 工具使用;- 以及
design_system(design-system manifest)、output(写输出文件)等模块。
它还拥有宿主策略:config 设置、transform-callback 运行时及其 memoization 键、输出写入,以及工具视图(inspect_file_source、token 建议 suggest_tokens)。Native 与 WASM 两个绑定层只负责桥接回调、序列化结果、执行 IO,都不重新实现编译器策略。
从 Cargo.toml 可以看到它是依赖最全的 crate:pandacss_codegen、pandacss_config、pandacss_encoder、pandacss_extractor、pandacss_fs、pandacss_literal、pandacss_project、pandacss_shared、pandacss_stylesheet、pandacss_system、pandacss_tokens、pandacss_utility 一应俱全,外加 fast-glob、lru、regex、rustc-hash、thiserror、tracing 等通用设施。这种"下游依赖全、但全部向一个方向"的形状,把项目状态、CSS 发射、代码生成与文件系统原语保持在互相独立的低层关切上,同时让它们的组合在一个高层 crate 中显式可见。
未来 crate 的边界纪律
文档明确规定:不要保留空的占位 crate(Don't keep empty placeholder crates)。只有当实现真正存在时,才在它实际挣得的边界上添加 crate:
- CSS 优化器:只有存在真正的 CSS-aware optimizer 时,才新增 Tier 2 crate;
- 持久化缓存:只有存在真实的缓存行为时,才新增独立的基础设施/进程 crate。
换句话说,架构图上的空位不是"预留位置",而是"尚无实现、因此不应存在依赖关系"的信号。
合并问题常问常答(Standing answers)
文档把四个反复出现的设计疑问固化为"既定答案",避免重复讨论。这四条是理解分层设计意图的最佳注脚:
1. "pandacss_encoder + pandacss_recipes 是否应该合并成一个 core crate?" —— 不合并。依赖只朝一个方向(encoder 读取 Recipe;recipes 不知道 Encoder 的存在),而且 Tier-1 消费者不应传递性地拉进 smallvec / walker 机制。合并随时可逆,但合并之后再拆分干净代码就很痛苦。
2. "pandacss_project 是否应该拥有文件发现?" —— 现在不。glob 逻辑目前住在 pandacss_fs::FileSystem::glob(基于 fast-glob),由绑定层/JS 宿主显式调用(见 file_system.rs 约 74 行)。未来若落地 .gitignore 感知的遍历,应放在独立的 pandacss_discover crate 中,基于 pandacss_fs + ignore crate 构建。
3. "pandacss_project 是否应该修改源文件?" —— ParsedFile 保持只读(不是 ts-morph 的 SourceFile 类比,文件状态上没有 copy() / move() / applyTextChanges())。源码重写归 pandacss_transform 管:transform_source / transform_source_with 接收只读 &Config 与源文本,返回新字符串(+map)。它与 parse_file_with 共享 ParseTransforms 和 Config 的 class-name 辅助函数,但从不触碰项目构建/watch 状态。
4. "pandacss_extractor 是否应该知道 pandacss_tokens?" —— 应该,但只在一个窄点上:pandacss_extractor 依赖 pandacss_tokens,是为了让静态求值器能折叠 token('colors.red.500') 调用。依赖是单向的;pandacss_tokens 完全不知道 pandacss_extractor 的存在。
为什么依赖必须是单向的:extract → encode → emit
Panda v2 本质上是一条单向流水线:
extract → encode → emit
crate 布局把这一方向显式地暴露在依赖层。当被问到"功能 X 应该放在哪里"时,文档给出的方法是追踪数据流向:
| 新增功能类型 | 归属 |
|---|---|
对 Literal 形状的新解析 |
Tier 1(pandacss_recipes、pandacss_tokens,或新兄弟 crate) |
| 新的已解析配置输入形状 | Tier 1(pandacss_config)+ Tier 3 的编译(pandacss_system::System) |
| 发射 atom 的新遍历 | Tier 2(扩展 pandacss_encoder) |
| 从 atoms/recipes/静态 CSS 的新 CSS 发射 | Tier 2(pandacss_stylesheet) |
| 发射后的新 CSS 优化 | 未来的 Tier 2 CSS-aware 优化器 |
| 给定配置下的新只读查询 | Tier 3(pandacss_system) |
| 拥有多文件状态的新横切编排 | Tier 4(pandacss_project) |
| 新的 I/O 或变更操作 | 大概率是新 crate,而非扩展现有 crate |
这套决策表把"数据朝哪个方向走"作为第一性问题:数据往哪层走,功能就往哪层放。它与 crates/RUST_GUIDE.md 中"先看 Layer 3(Panda domain / WHY)再动手"的工程方法互相呼应——当你问出"这段代码该放哪"时,RUST_GUIDE 会直接指引你阅读本文档。
延伸阅读
要深入每一层的具体设计,仓库内已有配套设计笔记:
- design-notes/filesystem.md ——
pandacss_fs::FileSystem的完整设计(os / memory 双实现); - design-notes/astro-parser.md ——
pandacss_sfc对 Astro 的 token 化方案; - design-notes/stylesheet.md —— stylesheet 作为 emitter/压缩写入器的边界定义;
- design-notes/project-lifecycle.md ——
Project的生命周期与可变状态; - design-notes/transformer/README.md ——
pandacss_transform的源码变换模型; - design-notes/scope-and-boundaries.md —— 引擎整体的范围与边界;
- crates/RUST_GUIDE.md —— Rust 编码规范与评审清单(含 tier 违反检查)。
理解这套分层之后,无论你是阅读该仓库的 crates/ 代码、评审 Rust 变更,还是计划向引擎贡献新能力,"功能该放哪个 crate"这个问题都能用同一条数据流逻辑快速回答。