Penpot 共享层解析:common/ 目录的 CLJC 架构、命名空间分层规则与跨运行时设计约束
本文以仓库内的架构记忆文档 .serena/memories/common/core.md 为主体,完整展开 Penpot 共享代码层 common/ 的命名空间地图、分层抽象规则与跨运行时(JVM + CLJS)约束,并结合 common/src/app/common 下的真实源码与 common/deps.edn 中的依赖事实进行纵深印证。读完后你能掌握:app.common.* 各命名空间的职责边界、"泛型数据层不感知业务域"的分层纪律如何在源码中落地,以及组织/团队权限这类跨端共享逻辑(fail-closed 规则)的实际实现。
一、common/ 是什么:一份代码,多个运行时
Penpot 的前端(浏览器端编辑器)、后端(Clojure 服务端)、exporter(导出服务)以及库/文件工具链,都依赖同一份共享逻辑。这份共享代码放在 common/ 目录,使用 CLJC(Clojure/ClojureScript) 编写——同一份 .cljc 源码既编译到 JVM 又编译到 CLJS。正如架构记忆文档所述:"shared CLJC for frontend, backend, exporter, library/file tooling, tests. Small semantic changes can affect multiple runtimes"(共享给前端、后端、exporter、库/文件工具与测试;一处微小的语义变更可能影响多个运行时)。
这不是修辞,而是 common/deps.edn 中可见的工程事实:
org.clojure/clojure1.12.5 与org.clojure/clojurescript1.12.145 同时出现,确认同一模块需要双运行时构建;metosin/malli0.20.1 与expound0.9.0 提供跨端一致的数据校验(Malli 同时支持 JVM 与 CLJS,这是共享 schema 层能存在的前提);com.cognitect/transit-clj与com.cognitect/transit-cljs成对出现,对应同一序列化格式在两个运行时的实现;- 测试别名
:test使用 kaocha(-m kaocha.runner),配合 JS 侧测试入口(pnpm run test:quiet),形成"同一 CLJC 测试、双端各跑一遍"的验证方式。
正因"一份语义、多个消费者",对 common/ 的任何修改都应默认考虑前端、后端、exporter 三个方向的兼容性影响。
二、稳定的命名空间地图(Stable Namespace Map)
架构文档给出了 app.common.* 的职责划分,以下逐条对照 common/src/app/common 的实际目录结构验证:
1. app.common.data 与 app.common.data.macros:与业务域无关的通用数据工具
对应文件 common/src/app/common/data.cljc 与 common/src/app/common/data/macros.cljc,以及同目录的 common/src/app/common/data/undo_stack.cljc。
以 macros.cljc 为例,可以看到这一层"不依赖 Penpot 域实体"的具体含义:
select-keys:clojure.core/select-keys的宏版本,当键集合在编译期已知时展开为逐个get,注释标明可获得约 600% 的性能提升,且语义上与核心版略有差异(不会删除不存在的键);get-in:宏版本get-in,键向量为常量时编译为->链式get,注释标明 20-40% 的性能提升;export:通过 reader conditional(#?(:clj ...))在编译期分别为 CLJS/CLJ 生成"再导出"代码,CLJS 分支甚至调用cljs.analyzer.api解析目标 var 的元数据。
这些工具全部操作"任意 map/向量",完全不知道 shape、component、file 为何物——这正是分层规则第一条的活例证。
2. app.common.types.*:单实体域的类型、schema 与谓词
common/src/app/common/types 目录实际包含 27 个命名空间:file.cljc、page.cljc、shape.cljc、shape_tree.cljc、component.cljc、variant.cljc、token.cljc、tokens_lib.cljc、typography.cljc、grid.cljc、color.cljc、fills.cljc、stroke.cljc、text.cljc、path.cljc、library.cljc、project.cljc、team.cljc、organization.cljc、profile.cljc、font.cljc、plugins.cljc 等。每个命名空间守住一个领域实体的 schema、谓词与"实体局部操作"。
架构文档特别点名的 types/organization.cljc 值得精读,因为它完整演示了"types.* 保存单实体不变量 + fail-closed 权限规则":
schema:organization(第 12-27 行):定义了组织实体的 Malli schema,核心字段:id、:name、:slug、:owner-id、:avatar-bg-url,以及可选的:permissions子 map——:create-teams("any"|"onlyMe")、:delete-teams("onlyMe"|"onlyOwners")、:move-teams("always"|"myOrganizations"|"never")、:new-team-members("anyone"|"members")。由于组织逻辑同时运行在浏览器与 JVM 上,权限判断必须共享实现,这正是该 schema 放在common/而非后端的理由。apply-organization(第 40-56 行):把组织字段以嵌套:organizationmap 形式合并进 team map。实现细节很讲究——对每个organization->team-keys中的字段,值非 nil 则assoc,否则dissoc,从而正确处理"挂接组织(字段全有)"与"解绑组织(org 为 nil 或字段全缺)"两个方向。- fail-closed 权限规则(第 78-176 行):
defaults给出五个权限键的保守默认值(如:delete-teams "onlyOwners"、:send-invitations "ownersAndAdmins");action-rules将:create-team、:delete-team、:move-team、:send-invitations、:add-anybody-to-team五个动作映射到各自的 check 函数;allowed?的文档字符串直接写明 "Returns true only for explicitly allowed actions (fail-closed)"——未知动作一律返回 false。而can-send-invitations?(第 164 行起)展示了共享逻辑如何读取功能开关:仅当flags/*current*包含:admin-console且团队挂了组织时才走组织级规则,否则回退到团队级 owner/admin 判断。这种"同一谓词、双端可用、显式降级"的写法是types.*层的典型形态。
3. app.common.files.*:文件级操作、shape 树、变更应用与迁移
common/src/app/common/files 目录包含 16 个文件:changes.cljc(变更应用)、changes_builder.cljc(变更构建器)、migrations.cljc(文件数据迁移)、validate.cljc(校验)、repair.cljc(修复)、indices.cljc、page_diff.cljc、shapes_builder.cljc、shapes_helpers.cljc、builder.cljc、defaults.cljc、comp_processors.cljc、tokens.cljc、variant.cljc、focus.cljc、stats.cljc。它们承担架构文档所说的"file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic"。
配套的聚焦记忆文档 changes-architecture.md 补充了变更记录的形状(:add-obj/:mod-obj/:del-obj、:add-component 等家族)与 changes-builder 的高频 API(pcb/empty-changes、pcb/update-shapes、pcb/add-objects 等),并强调测试应通过 thf/apply-changes 走生产变更管线,而非直接改对象 map。
4. app.common.logic.*:跨实体的较高层工作流/算法
common/src/app/common/logic 目录现有五个命名空间:libraries.cljc、shapes.cljc、tokens.cljc、variants.cljc、variant_properties.cljc,对应文档中"files, shapes, components, variants, libraries, tokens 之上的较高层 workflow/algorithm"。它与 types.* 的区别在于:允许协调一个文件内的多个实体,但仍不承载 UI 事件或后端 RPC 层面的业务流程。
5. app.common.geom.*:几何助手与变换
common/src/app/common/geom 目录包含 align.cljc、bounds_map.cljc、grid.cljc、line.cljc、matrix.cljc、point.cljc、rect.cljc、snap.cljc、shapes.cljc 以及子目录 shapes/(constraints.cljc、effects.cljc、fit_frame.cljc、flex_layout.cljc、grid_layout.cljc、min_size_layout.cljc、pixel_precision.cljc 等)。几何是设计工具的数值核心,flex_layout 与 grid_layout 子模块对应 Penpot 的弹性布局/网格布局能力。几何相关的不变量与坐标浮点比较细节,分别由 geometry-invariants.md 与 decimals-and-coordinates.md 两个聚焦记忆文档覆盖。
6. app.common.schema / app.common.schema.*:Malli 抽象层
common/src/app/common/schema.cljc 与 common/src/app/common/schema 子目录(desc_js_like.cljc、desc_native.cljc、generators.cljc、openapi.cljc、registry.cljc、test.cljc)构成对 Malli 的统一封装:::sm/uuid、::sm/text 等类型别名(organization.cljc 第 12-27 行的 schema 即建立在此层之上),openapi.cljc 支持从 schema 生成 OpenAPI 描述,test.cljc 则把 schema 校验接入测试。这层让所有 types.*、files.* 命名空间以一致方式声明与检查数据结构。
7. 跨运行时工具与测试助手
app.common.math、app.common.time、app.common.uuid、app.common.json:对应 math.cljc、time.cljc、uuid.cljc、json.cljc。注意 uuid.cljc 与 common/src/app/common/UUIDv8.java、common/src/app/common/uuid_impl.js 的组合——同一份 CLJC 逻辑通过平台特定实现文件(JVM 端 Java、JS 端 JavaScript)落地 UUID 生成,这是后文"reader conditional 规则"的典型用例。- common/src/app/common/weak 与 weak.cljc:弱引用容器的跨端抽象,
impl_weak_map.js/impl_loadable_weak_value_map.clj按运行时选择实现; app.common.test_helpers.*:common/src/app/common/test_helpers 目录提供生产路径测试助手——files.cljc(如sample-file、apply-changes)、components.cljc、variants.cljc、shapes.cljc、compositions.cljc、tokens.cljc、ids_map.cljc。据 testing.md,测试命名空间惯用thf/、tho/、thv/等短别名引用这些助手,且使用 label→uuid 助手的测试应以(t/use-fixtures :each thi/test-fixture)开头以便在每个用例间重置。
三、分层与跨运行时规则(Layering and Cross-Runtime Rules)
架构文档的核心纪律可归纳为两条,均能在源码中找到支撑。
平台特定代码必须用 reader conditional 隔离
"Use reader conditionals for platform-specific code. Because CLJC runs on JVM and CLJS targets, avoid assuming browser-only or JVM-only behavior unless the reader conditional isolates it."
源码实例:macros.cljc 第 10-14 行用 #?(:cljs (:require-macros ...)) 处理宏自身在 CLJS 下的加载;第 12-14 行 #?(:clj [cljs.analyzer.api :as aapi] :clj [clojure.core ...] :cljs [cljs.core ...]) 在同一 :require 中按运行时选择依赖;export 宏的整个定义被 #?(:clj ...) 包裹(第 54 行起),因为它本身就是编译期工具,仅在 JVM 编译 CLJS 源码时生效。weak.cljc 按运行时分发到 impl_weak_map.js 或 JVM 端实现,也是同一模式。
抽象方向必须自低向高保持
文档给出了五层职责方向(新代码与重构都应遵守):
- 泛型数据工具不感知 Penpot 域概念——
app.common.data*只处理任意集合/map(见第二节第 1 点); types.*守住单个域实体或 ADT 的不变量——如organization.cljc只围绕组织实体的 schema 与权限谓词;files.*可协调一个文件内的多个实体并维持引用完整性——变更应用、校验、修复都发生在此层;changes*应把可序列化的变更记录适配为低层操作,避免在其中内嵌宽泛业务算法——变更记录本身是持久化载荷与撤销/重做基础(见 changes-architecture.md 的 "A change set is both the persistence payload and the basis for undo/redo");logic.*与前端/后端事件层拥有更高层的 workflow/业务行为。
文档同时提醒:"Some legacy code violates this layering; do not copy those violations into new code when a focused refactor is practical."——遗留代码存在违反分层的情况,但不应把违反扩散到新代码。
四、记忆路由:修改 common/ 前该读哪份聚焦文档
架构文档的 "Focused memory routing" 节把 common/ 的细粒度知识分发到 13 份聚焦记忆。下表完整继承该路由,并标注对应源文件位置,便于按图索骥:
| 领域 | 聚焦记忆(相对仓库根目录) | 覆盖内容 |
|---|---|---|
| 模型/持久化形状 | data-model-change-checklist.md | 文件/页面/shape/组件属性变更的跨模块检查清单、导入导出面、inspector/codegen |
| Token | tokens-schema-subtleties.md | token 数据结构、导入导出、active theme/set 语义、schema 强制转换行为 |
| 几何与布局 | geometry-invariants.md | shape 几何不变量、冗余几何字段、几何敏感测试 |
| 几何与布局 | decimals-and-coordinates.md | 坐标漂移与近似浮点比较 |
| 几何与布局 | layout-grid-subtleties.md | 布局/网格的 assign、deassign、元数据清理、自动定位 |
| 变更管线 | changes-architecture.md | 变更记录、undo/redo 架构、changes-builder API、生产路径变更指南 |
| 变更管线 | file-change-validation-migration-subtleties.md | 变更应用、shape 树编辑、校验/修复、迁移、second-pass touched 行为 |
| 组件/变体 | component-data-model.md | 组件/变体数据模型、ref 链、touched 覆盖语义、克隆路径 |
| 组件/变体 | component-swap-pipeline.md | 组件 swap、变体切换、keep-touched 管线 |
| 组件/变体 | component-debugging-recipes.md | 实时检查片段、临时运行时 patch、测试侧调试助手 |
| 文本与测试 | text-subtleties.md | 共享文本数据转换、DraftJS 兼容、现代文本内容、派生定位数据 |
| 文本与测试 | testing.md | 常用测试命令、助手约定、生产路径测试变更、运行时覆盖选择 |
| 全局测试纪律 | testing.md(memories 根级) | 跨切面测试原则、反模式、验证清单 |
这些记忆与源码目录一一对应:例如 changes-architecture.md 指向 files/changes.cljc 的 process-operation 多方法与 files/changes_builder.cljc;testing.md 给出的命令(从 common/ 目录执行 clojure -M:dev:test 跑 JVM 全量测试、pnpm run test:quiet 跑 JS 全量测试、--focus common-tests.logic.variants-switch-test 聚焦命名空间)与 common/deps.edn 的 :test 别名、common/scripts/test 等脚本直接呼应。
五、没有聚焦记忆的领域:以源码和测试为准
架构文档的最后一节明确列出"几乎没有专门记忆"的 common/ 领域:colors、media/SVG 助手、path 操作、缩略图助手、通用池、弱引用及部分工具命名空间。对照源码,这些正是 colors.cljc、media.cljc、common/src/app/common/svg(path.cljc 及 path/ 子目录)、thumbnails.cljc、generic_pool.clj、weak/ 等文件。文档给出的工作方式很明确:Treat work there as source/test-led unless a focused memory exists——在这些领域直接以源码与测试为主要依据推进,不要期待或虚构不存在的记忆文档。
六、把 common/ 改动落到验证:最小操作路径
综合 testing.md 与 common/deps.edn 的别名定义,验证 common/ 改动的标准动作(均在 common/ 目录下执行):
- JVM 全量:
clojure -M:dev:test(kaocha 驱动,别名:test定义于 deps.edn 第 75-77 行); - JS 全量:
pnpm run test:quiet(始终先构建再运行); - 聚焦单测:JVM 侧
clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch;JS 侧pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test,可追加--log-level warn控制日志; - 新增 JS 测试命名空间须登记到
common_tests/runner.cljc,已有命名空间新增 var 则无需改动; - 几何敏感测试先读 geometry-invariants.md,优先使用保持几何不变量的助手或生产变更助手,而非直接编辑单个字段。
七、小结
common/ 的价值不在于"共享"二字,而在于它把 Penpot 文件数据模型、几何、变更管线与权限规则收敛成一份跨 JVM/CLJS 的语义来源,并用严格的分层方向(data → types → files → changes → logic/事件层)与 reader conditional 纪律约束这份语义只在一处实现。对贡献者而言,架构文档给出的三条行动准则是:先按"记忆路由表"读对聚焦文档,再对照 common/src/app/common 对应命名空间动手;新代码保持抽象方向不自上而下泄漏;在没有聚焦记忆的区域,以源码与测试为唯一事实来源。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00