Panda CSS v2 Rust 引擎的 crate 分层架构:crates 工作区的一向依赖设计深度解析

原创2026-10-09 18:55:53993 阅读
文章标签:前端构建工具开发工具

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 的引用),并提供一个可选 serde feature(见 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 会直接指引你阅读本文档。

延伸阅读

要深入每一层的具体设计,仓库内已有配套设计笔记:

理解这套分层之后,无论你是阅读该仓库的 crates/ 代码、评审 Rust 变更,还是计划向引擎贡献新能力,"功能该放哪个 crate"这个问题都能用同一条数据流逻辑快速回答。

登录后查看全文
panda