Sim 前端导入规范实战绝对路径、桶文件导出与懒加载拆包策略【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim本文围绕 Sim 仓库中约束apps/sim主应用 TypeScript 导入方式的规则文件 .claude/rules/sim-imports.md 展开。该文档定义了四条硬规则——只用绝对导入、桶文件barrel优先、懒加载必须走深层路径并删除残留的桶文件再导出、禁止非桶文件二次导出——外加导入顺序与import type约定。读完本文你能理解每条规则背后的打包机制Turbopack 与 webpack 的解析差异、sideEffects缺失对代码拆分的影响、React 19 的 ref 转发行为并掌握配套守卫脚本scripts/check-import-specifiers.ts的工作原理与运行方式。规则的适用范围基于路径前缀的触发机制规则文件以 YAML frontmatter 开头声明了它作用的文件范围paths: - apps/sim/**/*.ts - apps/sim/**/*.tsx也就是说这套导入规范只约束apps/sim下的 TypeScript 源码——即 Sim 的 Next.js 主应用UI、API 路由、工作流编辑器参见 CLAUDE.md 中的目录结构说明。apps/realtime、apps/docs、packages/*不受该规则文件直接管辖。这个范围声明本身也透露了一个信息绝对导入依赖apps/sim/tsconfig.json中配置的/*路径别名而该别名只在这个 workspace 内成立。apps/sim/tsconfig.json 定义了规则所依赖的完整别名表{ extends: sim/tsconfig/nextjs.json, compilerOptions: { paths: { /*: [./*], /components/*: [./components/*], /lib/*: [./lib/*], /stores: [./stores], /stores/*: [./stores/*], /hooks/*: [./hooks/*], /blocks: [./blocks], /blocks/*: [./blocks/*], /providers/*: [./providers/*], /tools: [./tools], /tools/*: [./tools/*], /serializer/*: [./serializer/*], sim/db: [../../packages/db], sim/db/*: [../../packages/db/*], /executor: [./executor], /executor/*: [./executor/*] } } }注意两个细节所有别名都映射到 workspace 自身的相对目录.开头/stores/xxx等价于apps/sim/stores/xxx这就是绝对导入中绝对的含义——以应用根为锚点而非以当前文件为锚点sim/db直接指向../../packages/db的目录本身绕过了该包的exports字段。这意味着在apps/sim内部跨包导入既走 npm 的exports解析也走 tsconfigpaths的短路解析两种机制的行为可能不一致这也是后文守卫脚本需要按 workspace 各自解析的原因。底层解析模型继承自 packages/tsconfig/base.jsonmoduleResolution: bundler、module: ESNext、strict: true、noEmit: true再经 packages/tsconfig/nextjs.json 补充jsx: react-jsx等 Next.js 配置。bundler模式意味着导入路径从不要求写文件扩展名——这条设定与守卫脚本的检查逻辑直接相关下文会看到。规则一只用绝对导入禁止相对导入原文档给出的对比示例// ✓ Good import { useWorkflowStore } from /stores/workflows/store import { Button } from /components/ui/button // ✗ Bad import { useWorkflowStore } from ../../../stores/workflows/store禁止../../../式相对导入的动机很直接apps/sim的目录层级极深例如app/workspace/[workspaceId]/home/components/mothership-view/components/...这类路径相对导入在深层文件里既难读又难维护且文件移动后极易断裂而/别名导入与文件位置解耦重构时只需保证目标模块路径不变。仓库中的实际代码验证了这一约定。以工作流状态层的桶文件 apps/sim/stores/workflows/index.ts 为例它内部全部使用别名导入import { createLogger } from sim/logger import { getWorkflows } from /hooks/queries/utils/workflow-cache import { useWorkflowRegistry } from /stores/workflows/registry/store import { mergeSubblockState } from /stores/workflows/utils import { useWorkflowStore } from /stores/workflows/workflow/store import type { WorkflowState } from /stores/workflows/workflow/types注意最后一行WorkflowState只用到了类型因此使用import type——这正是规则文件中Type Imports一节的要求见后文类型导入在编译后会被完全擦除不产生运行时依赖边。为什么绝对路径也必须被自动校验看似无歧义的/路径在 Sim 的实际构建链里并不天然可靠。scripts/check-import-specifiers.ts 的头部注释解释了一个真实的坑next build走 webpacknext dev走 Turbopack两者解析同一种导入写法的行为不一致webpack 会通过resolve.extensionAlias把./errors.js重写到./errors.tsTurbopack 没有等价机制脚本注释中引用了上游问题单 vercel/next.js#82945。结果就是./foo.js指向.ts文件的写法在 CI 构建中绿灯但在每台开发机的 dev server 上 500由于 CI 不跑 Turbopack 模块图这类断裂对 CI 完全不可见。因此该脚本选择跑一次真实的解析而不是匹配某一种错误写法它覆盖错误扩展名、拼错的路径、文件移动后遗留的旧导入、失效的/别名以及sim/*包未导出的子路径。它的解析逻辑与规则文件中的导入约定是配套的——脚本逐 workspace 读取各自tsconfig.json的paths表因为/*在apps/sim与apps/realtime中含义不同按最长前缀优先匹配模拟 TypeScript 的优先级再对命中目标做base、baseext、base/indexext三档探测check-import-specifiers.ts 的 probe 实现。运行方式脚本注释中声明bun run scripts/check-import-specifiers.ts [--verbose]退出码为 0 表示全部第一方导入均可解析任一违规则退出 1 并逐条打印文件:行号 说明。扫描范围是apps/sim、apps/realtime、apps/docs、packages四个目录SCAN_DIRS 定义跳过测试文件与apps/*/scripts/**它们在 vitest/bun 下运行这两个环境本身能正确把.js解析到.ts。规则二3 个以上导出走桶文件从桶导入而非从单文件导入原文规定当一个文件夹有 3 个及以上导出时应建立index.ts桶文件消费方一律从桶导入// ✓ Good import { Dashboard, Sidebar } from /app/workspace/[workspaceId]/logs/components // ✗ Bad import { Dashboard } from /app/workspace/[workspaceId]/logs/components/dashboard/dashboard桶文件的价值在于给一个功能目录提供稳定的导入面消费方只依赖目录级 API目录内部怎么拆分、重命名文件都不影响外部。仓库里 apps/sim/stores/workflows/index.ts 就是典型形态——它把workflow/、registry/、subblock/等子目录的能力汇聚成/stores/workflows一个入口。但要注意桶优先不是无条件的。同一个守卫脚本维护了一个子路径强制名单SUBPATH_REQUIREDconst SUBPATH_REQUIRED new Set([sim/utils])对sim/utils这类包裸导入sim/utils会被判定为bare-barrel违规脚本会提示改从子路径导入例如sim/utils/id、sim/utils/helpers。原因是桶导入会把桶再导出的所有模块拖进依赖图——脚本的违规提示写得很直白one helper drags in the whole package — and one bad specifier anywhere inside it takes the importer down一个工具函数拖进整个包包里任何一处坏路径都会连累导入方。CLAUDE.md 中Common Utilities一节同样要求从sim/utils/id、sim/utils/retry等子路径取用共享工具与该守卫一致。所以完整的图景是两条方向相反、互为补充的规则场景规则理由应用内功能目录/stores、/app/.../components等3 导出建index.ts从桶导入稳定导入面隔离内部重组sim/utils等子路径优先的包必须从子路径导入禁止裸桶控制依赖图规模避免单点错误扩散规则三代码拆分时导入深层模块路径并删除残留的桶再导出这是原文档中最具技术密度的一节核心结论可以概括为三句话懒加载走深层路径、删掉死掉的桶再导出、用生产包 diff 验证而不是肉眼检查lazy()调用。机制为什么残留的export { X } from ./x会击穿拆分apps/sim/package.json中没有sideEffects: false字段可从 apps/sim/package.json 全文确认其 dependencies/devDependencies 之外不存在该声明。缺少这个声明时webpack 必须保守地假设每个模块都有副作用只要还有任何模块导入过某个桶文件桶文件里对重模块的再导出边就可能被保留在初始 chunk 中。于是形成这样的失效链条你用lazy(() import(...))把一个重组件拆出去但同目录的index.ts桶里仍留着export { Foo } from ./foo任何兄弟模块哪怕只是导入了桶里的另一个小组件只要导入该桶webpack 就可能保守地保留桶到./foo的边Foo及其全部传递依赖被拖回初始 chunk拆分静默失效——而lazy()调用本身看起来完全正确。原文给出的正确写法// ✓ Good — deep lazy import no barrel edge left behind const MothershipView lazy(() import(./components/mothership-view/mothership-view).then((m) ({ default: m.MothershipView })) ) // (and remove export { MothershipView } from ./mothership-view from components/index.ts)仓库中确实存在按此模式落地的代码。apps/sim/app/workspace/[workspaceId]/home/home.tsx 对 Mothership 主视图的拆分const MothershipView lazy(() import(./components/mothership-view/mothership-view).then((m) ({注意两个要点都被遵守导入的是./components/mothership-view/mothership-view这条深层路径而非桶命名导出通过.then((m) ({ default: ... }))适配成React.lazy要求的默认导出。因此完整的拆包操作顺序是把lazy的目标从桶改成深层路径 →删除桶文件中对该组件的再导出行 → 跑生产构建做 bundle diff 验证。原文强调最后一步不可省略仅凭阅读lazy()调用无法证明拆分生效只有产物对比才是证据。配套要求本地 Suspense 与 React 19 的 ref 行为原文还给出两条与懒加载直接相关的约束用就近的Suspense包裹懒加载组件。否则挂起状态会向上冒泡到页面级兜底整个路由闪烁一次React.lazy(memo(forwardRef(...)))在 React 19 中可以正确转发 DOMref但在 fallback 窗口期内ref.current为null所有消费方必须做空值保护if (!el) return/el?.。React 19这一前提在依赖中得到印证apps/sim/package.json 中react/react-dom锁定为19.2.4next为16.3.1。也就是说该规则是针对仓库当前实际 React 大版本写的行为说明而不是泛化的 React 知识——若未来升级 React 版本ref 转发与 Suspense 语义应重新核对。规则四非桶文件禁止再导出// ✓ Good - import from where its declared import { CORE_TRIGGER_TYPES } from /stores/logs/filters/types // ✗ Bad - re-exporting in utils.ts then importing from there import { CORE_TRIGGER_TYPES } from /app/workspace/.../utils这条规则与桶文件规则合起来构成一条清晰的导入图纪律再导出只允许发生在index.ts桶文件里。理由有二依赖图可审计消费方从声明源头导入时tsc悬停、跳转、删除文件等 IDE 行为与实际依赖边一一对应经过utils.ts之类的中转再导出后模块边界变得模糊重构时容易误判谁依赖谁避免万能 utils腐化CLAUDE.md 的 Utils Rules 一节有配套约束——单一消费方的 helper 不要建utils.ts直接内联2 个及以上文件需要同一 helper 才建。禁止在utils.ts里堆再导出正是防止它退化为杂物中转站。结合scripts/check-import-specifiers.ts看这条规则虽然没有专门的 lint 断言但它的反面导入指向不存在的目标会被该脚本兜住任何文件移动后忘记更新导入的断裂都会以unresolved违规的形式在 CI 中显形。导入顺序与类型导入约定原文规定了七层导入顺序React/core librariesExternal librariesUI componentssim/emcn、/components/uiUtilities/lib/...Stores/stores/...Feature importsCSS imports这套顺序的意图是让依赖方向在视觉上单向流动从最稳定、最通用的React 与三方库到最具体、最局部的当前 feature 自己的模块与样式。配合 Biome 作为统一格式化与 lint 工具biome.jsonapps/sim的lint脚本即biome check顺序约定可以被自动格式化部分兜底。类型导入则要求使用type关键字import type { WorkflowLog } from /stores/logs/types在moduleResolution: bundlerisolatedModules的组合下import type会被完全擦除它不产生运行时模块边因此一个纯类型模块如types.ts可以被任意模块安全引用而不进入产物。前文 stores/workflows/index.ts 第 6 行 的import type { WorkflowState }即标准用法。守卫脚本全景导入规则如何被 CI 兜底规则文件本身是人和 Agent的约束真正让规则不可违反的是 scripts/check-import-specifiers.ts 这类守卫。梳理它的检查矩阵违规类别触发条件输出示例unresolved第一方导入无法解析到真实文件错误扩展名、拼写错误、文件已移动、/别名失效、sim/*子路径未导出逐条打印文件:行号、说明原因对.js后缀误用额外提示 drop the extensionbare-barrel对SUBPATH_REQUIRED名单中的包当前为sim/utils使用裸桶导入提示改从某个具体子路径导入实现上有几个值得注意的工程细节按 workspace 解析路径别名每个 workspace 读取自己的tsconfig.jsonpaths最长前缀优先与 TypeScript 语义一致resolveViaPaths 实现sim/*包走exports字段解析读取各包package.json的exports表含./*: ./src/*通配子路径验证子路径确实有对应文件packageExports 实现注释先置空再扫描TSDoc 里出现的示例导入不算真实依赖边脚本把注释替换为等长空格以保持行号精确blankComments 实现静态import/export、动态import(...)、以及用于打破循环的require()三类边都检查import type因编译擦除而豁免。与导入主题相邻的还有一层守卫 scripts/check-client-boundary-imports.ts它检查 Next.js 的use client/use server边界禁止服务端求值的非 JSX 面route handler、prefetch、triggers、blocks从use client模块导入运行时值——这类导入在构建期无法被next build捕获只在 SSR/运行时以 X is not a function 的形式炸出。它同样依赖把/别名解析到真实源文件resolveSpecifier 实现。两个脚本合起来覆盖了导入规则的两大失效面解析层路径能否落到真实文件与边界层模块能否在该执行环境被引用。运行方式bun run scripts/check-import-specifiers.ts --verbose bun run scripts/check-client-boundary-imports.ts --check # CI 门禁模式快速检查清单在apps/sim中新增或修改导入时可以按此清单自检是否全部使用/或sim/*绝对导入没有任何./../../相对路径目标目录有 3 导出时是否从index.ts桶导入而不是深钻到单文件消费sim/utils时是否走子路径sim/utils/id等而非裸包名做lazy(() import(...))拆分时目标是否为深层模块路径桶中该组件的再导出是否已删除是否跑过生产构建并用 bundle diff 验证懒加载组件是否包在就近的Suspense中所有ref.current消费方是否做了空值保护非桶文件中是否避免了export { X } from ...中转导入是否按React → 三方库 → UI 组件 → lib 工具 → stores → feature → CSS七层排列纯类型引用是否使用了import type提交前bun run scripts/check-import-specifiers.ts是否通过相关文件索引规则本体.claude/rules/sim-imports.md路径别名与解析配置apps/sim/tsconfig.json、packages/tsconfig/nextjs.json、packages/tsconfig/base.json包声明无sideEffects字段React 19 / Next 16 依赖apps/sim/package.json桶文件实例apps/sim/stores/workflows/index.ts深层懒加载实例apps/sim/app/workspace/[workspaceId]/home/home.tsx导入解析守卫scripts/check-import-specifiers.ts客户端/服务端边界守卫scripts/check-client-boundary-imports.ts全局开发规范导入顺序、utils 规则、包管理器约定CLAUDE.md适用前提说明本文所有机制描述Turbopack 与 webpack 的extensionAlias差异、无sideEffects声明下的保守拆包、React 19 ref 转发均以当前仓库依赖版本Next 16.3.1、React 19.2.4、TypeScript bundler 解析模式为准升级上述任一依赖后相关行为应重新验证。【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考