Skip to content

perf(weapp-tailwindcss): 优化多构建器热路径缓存复用#819

Open
sonofmagic wants to merge 79 commits intomainfrom
perf/weapp-tailwindcss-hotpath-caching
Open

perf(weapp-tailwindcss): 优化多构建器热路径缓存复用#819
sonofmagic wants to merge 79 commits intomainfrom
perf/weapp-tailwindcss-hotpath-caching

Conversation

@sonofmagic
Copy link
Owner

@sonofmagic sonofmagic commented Mar 10, 2026

变更说明

  • 优化 Vite bundle 增量状态与 CSS/HTML 热路径的缓存复用
  • 优化 core、gulp、webpack、uni-app-x 的默认 handler options 复用,减少重复对象构造
  • 优化 JS literal 处理、runtime/cache、compiler context cache 与 lightningcss 固定成本
  • 补充 bundler、core、context、runtime、lightningcss 的回归测试

验证

  • pnpm exec vitest run test/bundlers/vite-bundle-state.unit.test.ts test/bundlers/vite-plugin.bundle.unit.test.ts test/bundlers/vite-plugin.incremental-issue-33.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/bundlers/vite-plugin.uni-app-x.unit.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/js/handlers-branches.test.ts test/js/handlers-stale-fallback-regression.test.ts test/js/framework-dynamic-class-regression.test.ts test/js/index-handler.test.ts test/bundlers/vite-plugin.incremental-issue-33.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/context-cache.test.ts test/context/index.test.ts test/context/refresh.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/tailwindcss/runtime-cache.test.ts test/tailwindcss/runtime-hot-update.test.ts test/tailwindcss/runtime.test.ts test/bundlers/vite-plugin.bundle.unit.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/lightningcss/style-handler.test.ts test/core.test.ts test/postcss/style.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/bundlers/gulp.unit.test.ts test/bundlers/vite-plugin.bundle.unit.test.ts test/bundlers/webpack.v5.unit.test.ts test/bundlers/webpack.v4.unit.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/core.transform-wxss-options.unit.test.ts test/core.test.ts --config ./vitest.config.ts
  • pnpm exec vitest run test/bundlers/gulp.unit.test.ts test/bundlers/vite-plugin.uni-app-x.unit.test.ts test/bundlers/webpack.v5.unit.test.ts test/bundlers/webpack.v4.unit.test.ts --config ./vitest.config.ts
  • pnpm exec vitest bench test/core.bench.ts --config ./vitest.config.ts

Summary by Sourcery

改进热路径缓存以及在各打包器和核心转换中的默认处理器选项复用,以减少增量构建中的重复计算。

Enhancements:

  • 优化 Vite 打包构建状态跟踪和依赖映射,更好地在增量运行中复用 JS/CSS/HTML 处理结果。
  • 在 core、gulp、webpack、Vite 和 uni-app-x 集成中缓存并复用默认的 style/template/js 处理器选项,避免为每个资源或构建重新构造 override 对象。
  • 优化 JS 字面量类名转换,在同一个字面量内复用解析结果,减少重复的转义操作。
  • 调整编译器上下文缓存键策略,将基于稳定选项的上下文与运行时作用域探测分离,同时避免不必要的调用方位置检查。
  • 当 override 选项为空时复用 lightningcss 的预处理工作,以尽量降低每次调用的固定开销。
  • 通过在单个事件循环周期内缓存运行时配置签名并在配置变更时精确失效,限制 Tailwind 配置的 stat 调用次数。

Tests:

  • 添加 Vite 的打包状态单元测试,以验证增量依赖跟踪和关联入口失效逻辑。
  • 扩展 core、gulp、webpack、Vite、uni-app-x、lightningcss、JS 处理器、上下文缓存以及运行时缓存的测试,以覆盖处理器选项复用、运行时变更和签名缓存行为。
Original summary in English

Summary by Sourcery

Improve hot-path caching and default handler option reuse across bundlers and core transforms to reduce repeated computation in incremental builds.

Enhancements:

  • Refine Vite bundle build state tracking and dependency mapping to better reuse JS/CSS/HTML processing across incremental runs.
  • Cache and reuse default style/template/js handler options in core, gulp, webpack, Vite, and uni-app-x integrations to avoid reconstructing override objects per asset or build.
  • Optimize JS literal class name transformation to reuse resolution results within a literal and reduce redundant escaping work.
  • Adjust compiler context cache keying to separate stable option-based contexts from runtime scope probes while avoiding unnecessary caller-location inspection.
  • Reuse lightningcss preparation work when override options are empty to minimize fixed per-call overhead.
  • Limit Tailwind config stat calls by caching runtime config signatures within an event loop turn and invalidating precisely on config changes.

Tests:

  • Add bundle state unit tests for Vite to validate incremental dependency tracking and linked entry invalidation.
  • Extend core, gulp, webpack, Vite, uni-app-x, lightningcss, JS handler, context cache, and runtime cache tests to cover handler option reuse, runtime changes, and signature caching behaviors.

@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2026

🦋 Changeset detected

Latest commit: 1f0742f

The changes in this PR will be included in the next version bump.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 10, 2026

Reviewer's Guide

将 Vite bundle 的增量状态跟踪重构到一个独立的 bundle-state 模块中,新增跨打包器的 handler 选项对象缓存和 Tailwind runtime/config 签名缓存,并扩展编译器上下文缓存以及 JS 字面量处理,以减少热点路径上的重复分配和文件系统操作,同时在不同打包器、上下文、运行时和 lightningcss 上添加针对性的回归测试。

使用 bundle-state 和 handler 选项缓存的 Vite generateBundle 时序图

sequenceDiagram
  participant ViteCompiler as ViteCompiler
  participant GenerateBundle as generateBundle_hook
  participant BundleState as bundle_state_module
  participant Runtime as runtime_classset
  participant TemplateHandler as templateHandler
  participant StyleHandler as styleHandler

  ViteCompiler->>GenerateBundle: generateBundle(bundle)

  GenerateBundle->>BundleState: buildBundleSnapshot(bundle, opts, outDir, state, disableDirtyOptimization)
  BundleState-->>GenerateBundle: BundleSnapshot(entries, jsEntries, processFiles, changedByType)

  GenerateBundle->>Runtime: ensureRuntimeClassSet(forceRuntimeRefresh)
  Runtime-->>GenerateBundle: runtimeSet

  GenerateBundle->>GenerateBundle: getCssHandlerOptions(file) using cssHandlerOptionsCache

  loop for_each_html_asset
    GenerateBundle->>TemplateHandler: templateHandler(rawSource, defaultTemplateHandlerOptions)
    TemplateHandler-->>GenerateBundle: transformed_html
  end

  loop for_each_css_asset
    GenerateBundle->>StyleHandler: styleHandler(rawSource, getCssHandlerOptions(file))
    StyleHandler-->>GenerateBundle: { css }
  end

  GenerateBundle->>BundleState: updateBundleBuildState(state, snapshot, linkedByEntry)
  BundleState-->>GenerateBundle: state_updated

  GenerateBundle-->>ViteCompiler: updated_bundle_assets
Loading

更新后的 Vite bundle-state 与 build snapshot 类图

classDiagram
  class BundleStateEntry {
    +string file
    +OutputAsset~or~OutputChunk output
    +string source
    +EntryType type
  }

  class ProcessFileSets {
    +Set~string~ html
    +Set~string~ js
    +Set~string~ css
  }

  class BundleSnapshot {
    +BundleStateEntry[] entries
    +Map~string,OutputEntry~ jsEntries
    +Map~string,string~ sourceHashByFile
    +Record~EntryType,Set~string~~ changedByType
    +ProcessFileSets processFiles
    +Map~string,Set~string~~ linkedImpactsByEntry
  }

  class BundleBuildState {
    +number iteration
    +Map~string,string~ sourceHashByFile
    +Map~string,Set~string~~ linkedByEntry
    +Map~string,Set~string~~ dependentsByLinkedFile
  }

  class bundle_state_module {
    +EntryType classifyBundleEntry(file, opts)
    +BundleBuildState createBundleBuildState()
    +BundleSnapshot buildBundleSnapshot(bundle, opts, outDir, state, forceAll)
    +updateBundleBuildState(state, snapshot, linkedByEntry)
  }

  class generate_bundle_module {
    +createGenerateBundleHook(context)
  }

  BundleSnapshot "*" --> "1" ProcessFileSets : aggregates
  BundleSnapshot "*" --> "*" BundleStateEntry : contains
  BundleBuildState "1" --> "*" BundleStateEntry : tracks_previous
  bundle_state_module ..> BundleSnapshot : creates
  bundle_state_module ..> BundleBuildState : creates_updates
  generate_bundle_module ..> BundleSnapshot : uses
  generate_bundle_module ..> BundleBuildState : updates
Loading

更新后的核心上下文转换辅助方法与 handler 选项缓存类图

classDiagram
  class CreateContextResult {
    +function transformWxss(rawCss, options)
    +function transformJs(rawJs, options)
    +function transformWxml(rawWxml, options)
  }

  class RuntimeState {
    +TailwindcssPatcherLike twPatcher
    +Promise~void~ patchPromise
  }

  class CoreContextModule {
    -Map~number,CreateJsHandlerOptions~ defaultJsHandlerOptionsCache
    -Map~number_or_unknown,Partial~IStyleHandlerOptions~~ defaultStyleHandlerOptionsCache
    -Set~string~ cachedDefaultTemplateRuntimeSet
    -ITemplateHandlerOptions cachedDefaultTemplateHandlerOptions
    +resolveTransformWxssOptions(options)
    +resolveTransformJsOptions(options)
    +resolveTransformWxmlOptions(options)
    +withRuntimeTailwindMajorVersion(options)
    +runtimeAwareTemplateJsHandler(source, runtime, handlerOptions)
    +createContext(options) CreateContextResult
  }

  class JsHandler {
    +jsHandler(source, runtimeSet, options)
  }

  class StyleHandler {
    +styleHandler(rawCss, options)
  }

  class TemplateHandler {
    +templateHandler(rawWxml, options)
  }

  CreateContextResult ..> RuntimeState : holds
  CoreContextModule ..> CreateContextResult : creates
  CoreContextModule ..> JsHandler : wraps_and_caches_options
  CoreContextModule ..> StyleHandler : wraps_and_caches_options
  CoreContextModule ..> TemplateHandler : wraps_and_caches_options
Loading

文件级变更

Change Details Files
将 Vite generateBundle 的热点路径重构为可复用的 bundle-state snapshot API,并提升 JS 依赖跟踪的准确性。
  • 将入口分类、哈希计算、脏检查以及 JS 入口收集提取到 bundlers/vite/bundle-state.ts 中,并引入 BundleSnapshotBundleBuildState 类型。
  • buildBundleSnapshot/updateBundleBuildState 替换 generate-bundle.ts 中内联的脏入口计算逻辑,包括新增 dependentsByLinkedFile 映射,用于在关联模块变更时重新入队 JS 入口。
  • 在 generateBundle 中使用预先计算好的 snapshot.entries/source 字符串、jsEntrieslinkedImpactsByEntry 来避免重复计算源码和关联影响签名;并将调试日志统一标准化为基于 snapshot.changedByType
packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts
packages/weapp-tailwindcss/src/bundlers/vite/bundle-state.ts
packages/weapp-tailwindcss/test/bundlers/vite-bundle-state.unit.test.ts
在不同打包器间缓存并复用 style/template handler 选项对象,以减少对象抖动以及下游哈希/指纹计算开销。
  • 在 Vite generate bundle、Webpack v4/v5 资源钩子以及 uni-app-x Vite 插件中添加按文件维度的 CSS handler 选项缓存;在多次运行之间复用缓存的 isMainChunk/postcssOptions/majorVersion 对象。
  • 在核心上下文、Webpack 钩子和 gulp 插件中引入默认的 template handler 选项对象(runtimeSet, jsHandler),只要 runtimeSet 保持稳定就重用它们。
  • 添加测试,确保对 Vite、Webpack v4/v5、gulp 和 uni-app-x 路径上的重复 CSS/HTML 资源,handler 选项对象的身份(引用)得以复用。
packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts
packages/weapp-tailwindcss/src/bundlers/webpack/BaseUnifiedPlugin/v4-assets.ts
packages/weapp-tailwindcss/src/bundlers/webpack/BaseUnifiedPlugin/v5-assets.ts
packages/weapp-tailwindcss/src/uni-app-x/vite.ts
packages/weapp-tailwindcss/src/bundlers/gulp/index.ts
packages/weapp-tailwindcss/src/core.ts
packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/webpack.v4.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/webpack.v5.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/gulp.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts
packages/weapp-tailwindcss/test/core.transform-wxss-options.unit.test.ts
改进编译器上下文缓存键的计算与复用,在保持项目隔离的前提下跨运行时作用域复用缓存。
  • 基于 runtime scope 序列化结果,引入使用 WeakMap 和 Map 的多层记忆化(memoization)策略,用于缓存编译器上下文缓存键,避免在相同 options/运行环境下重复执行 normalize+hash。
  • 抽取 runtime 缓存作用域的推导逻辑,使其尊重显式的 tailwindcssBasedir(禁用调用方位置探测),并扩展环境输入(INIT_CWDUNI_* 环境变量)。
  • 优化对 Set/Map 值的归一化处理,改为使用预先计算的排序键,而不是基于 JSON-stringify 的比较器,并新增测试以覆盖在显式 tailwindcssBasedir 情况下上下文复用的行为。
  • 确保 getRuntimeCacheScope 仅在不存在显式或基于环境变量的 basedir 时,才把调用方信息包含进缓存键。
packages/weapp-tailwindcss/src/context/compiler-context-cache.ts
packages/weapp-tailwindcss/test/context-cache.test.ts
优化 JS 字面量处理与 Babel 流程,避免重复的类名转换工作。
  • js/handlers.ts 中引入 CandidatePlan 缓存,用于在单个字面量内部对每个候选类的解析结果(决策 + 替换值)进行记忆化,避免对同一候选多次调用 resolveClassNameTransformWithResult 和重复计算替换内容。
  • 调整 replaceHandleValue 以使用 candidate plans,并在重复的转义 runtime-set 命中场景下保持稳定性;为同一模板字面量中重复候选的情况添加针对性回归测试。
  • 更新 js/babel/process.ts,为 StringLiteralTemplateElement 路径预先计算各自独立的 handler 选项对象,避免对每个节点做对象展开,并强制保持 needEscaped 语义的一致性。
packages/weapp-tailwindcss/src/js/handlers.ts
packages/weapp-tailwindcss/src/js/babel/process.ts
packages/weapp-tailwindcss/test/js/handlers-branches.test.ts
packages/weapp-tailwindcss/test/js/handlers-stale-fallback-regression.test.ts
降低 lightningcss handler 的固定成本,并确保在有/无覆盖选项时行为稳定一致。
  • 重构 createLightningcssStyleHandler,预先计算一个基础的 PreparedLightningcssRuntime(options、visitor、specificity replacer),只有在提供非空 override 选项时才重新构建。
  • 确保空的或省略的 overrideOptions 产生完全相同的输出(代码、source map、警告),并为此新增回归测试以验证稳定性。
  • 保持 prepareStyleOptions 作为选项归一化的单一事实来源,同时在多次调用之间复用 visitor/replaceSpecificity
packages/weapp-tailwindcss/src/lightningcss/style-handler.ts
packages/weapp-tailwindcss/test/lightningcss/style-handler.test.ts
收紧 Tailwind runtime 缓存,通过在事件循环内以及失效时缓存 config stat 签名。
  • 按配置路径添加 runtimeConfigSignatureCache,配合基于定时器的清理策略,使得 fs.statSync 结果只在同一个事件循环轮次内复用,之后清掉以便检测后续文件变更。
  • 确保 invalidateRuntimeClassSet 也会清除对应配置路径的签名缓存条目,从而强制在下次访问时重新 stat。
  • 新增测试,验证在单个 tick 内复用签名、跨 tick 重新 stat,以及显式失效后立即重新 stat 的行为。
packages/weapp-tailwindcss/src/tailwindcss/runtime/cache.ts
packages/weapp-tailwindcss/test/tailwindcss/runtime-cache.test.ts

Tips and commands

Interacting with Sourcery

  • 触发新的 Review: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评论即可继续对话。
  • 从 Review 评论生成 GitHub Issue: 在某条 Review 评论下回复,要求 Sourcery 从该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建一个 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题中任意位置写入 @sourcery-ai 即可随时生成标题。也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 总结: 在 Pull Request 描述正文的任意位置写入 @sourcery-ai summary,即可在对应位置生成 PR 总结。也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成总结。
  • 生成 Reviewer's Guide: 在 Pull Request 中评论 @sourcery-ai guide,即可随时(重新)生成 Reviewer's Guide。
  • 一次性解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论,又不想再看到它们,这会很有用。
  • 一次性关闭所有 Sourcery Reviews: 在 Pull Request 中评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery Reviews。尤其适合你希望从一个全新的 Review 开始——别忘了随后再评论 @sourcery-ai review 来触发新的 Review!

Customizing Your Experience

打开你的 dashboard 以:

  • 启用或禁用诸如 Sourcery 自动生成的 Pull Request 总结、Reviewer's Guide 等 Review 功能。
  • 修改 Review 语言。
  • 添加、移除或编辑自定义 Review 说明。
  • 调整其他 Review 设置。

Getting Help

Original review guide in English

Reviewer's Guide

Refactors Vite bundle incremental state tracking into a dedicated bundle-state module, adds cross-bundler caching of handler option objects and Tailwind runtime/config signatures, and extends compiler context caching and JS literal handling to reduce repeated allocations and filesystem work along hot paths while adding focused regression tests across bundlers, context, runtime, and lightningcss.

Sequence diagram for Vite generateBundle with bundle-state and handler option caching

sequenceDiagram
  participant ViteCompiler as ViteCompiler
  participant GenerateBundle as generateBundle_hook
  participant BundleState as bundle_state_module
  participant Runtime as runtime_classset
  participant TemplateHandler as templateHandler
  participant StyleHandler as styleHandler

  ViteCompiler->>GenerateBundle: generateBundle(bundle)

  GenerateBundle->>BundleState: buildBundleSnapshot(bundle, opts, outDir, state, disableDirtyOptimization)
  BundleState-->>GenerateBundle: BundleSnapshot(entries, jsEntries, processFiles, changedByType)

  GenerateBundle->>Runtime: ensureRuntimeClassSet(forceRuntimeRefresh)
  Runtime-->>GenerateBundle: runtimeSet

  GenerateBundle->>GenerateBundle: getCssHandlerOptions(file) using cssHandlerOptionsCache

  loop for_each_html_asset
    GenerateBundle->>TemplateHandler: templateHandler(rawSource, defaultTemplateHandlerOptions)
    TemplateHandler-->>GenerateBundle: transformed_html
  end

  loop for_each_css_asset
    GenerateBundle->>StyleHandler: styleHandler(rawSource, getCssHandlerOptions(file))
    StyleHandler-->>GenerateBundle: { css }
  end

  GenerateBundle->>BundleState: updateBundleBuildState(state, snapshot, linkedByEntry)
  BundleState-->>GenerateBundle: state_updated

  GenerateBundle-->>ViteCompiler: updated_bundle_assets
Loading

Updated class diagram for Vite bundle-state and build snapshot

classDiagram
  class BundleStateEntry {
    +string file
    +OutputAsset~or~OutputChunk output
    +string source
    +EntryType type
  }

  class ProcessFileSets {
    +Set~string~ html
    +Set~string~ js
    +Set~string~ css
  }

  class BundleSnapshot {
    +BundleStateEntry[] entries
    +Map~string,OutputEntry~ jsEntries
    +Map~string,string~ sourceHashByFile
    +Record~EntryType,Set~string~~ changedByType
    +ProcessFileSets processFiles
    +Map~string,Set~string~~ linkedImpactsByEntry
  }

  class BundleBuildState {
    +number iteration
    +Map~string,string~ sourceHashByFile
    +Map~string,Set~string~~ linkedByEntry
    +Map~string,Set~string~~ dependentsByLinkedFile
  }

  class bundle_state_module {
    +EntryType classifyBundleEntry(file, opts)
    +BundleBuildState createBundleBuildState()
    +BundleSnapshot buildBundleSnapshot(bundle, opts, outDir, state, forceAll)
    +updateBundleBuildState(state, snapshot, linkedByEntry)
  }

  class generate_bundle_module {
    +createGenerateBundleHook(context)
  }

  BundleSnapshot "*" --> "1" ProcessFileSets : aggregates
  BundleSnapshot "*" --> "*" BundleStateEntry : contains
  BundleBuildState "1" --> "*" BundleStateEntry : tracks_previous
  bundle_state_module ..> BundleSnapshot : creates
  bundle_state_module ..> BundleBuildState : creates_updates
  generate_bundle_module ..> BundleSnapshot : uses
  generate_bundle_module ..> BundleBuildState : updates
Loading

Updated class diagram for core context transform helpers and handler options caching

classDiagram
  class CreateContextResult {
    +function transformWxss(rawCss, options)
    +function transformJs(rawJs, options)
    +function transformWxml(rawWxml, options)
  }

  class RuntimeState {
    +TailwindcssPatcherLike twPatcher
    +Promise~void~ patchPromise
  }

  class CoreContextModule {
    -Map~number,CreateJsHandlerOptions~ defaultJsHandlerOptionsCache
    -Map~number_or_unknown,Partial~IStyleHandlerOptions~~ defaultStyleHandlerOptionsCache
    -Set~string~ cachedDefaultTemplateRuntimeSet
    -ITemplateHandlerOptions cachedDefaultTemplateHandlerOptions
    +resolveTransformWxssOptions(options)
    +resolveTransformJsOptions(options)
    +resolveTransformWxmlOptions(options)
    +withRuntimeTailwindMajorVersion(options)
    +runtimeAwareTemplateJsHandler(source, runtime, handlerOptions)
    +createContext(options) CreateContextResult
  }

  class JsHandler {
    +jsHandler(source, runtimeSet, options)
  }

  class StyleHandler {
    +styleHandler(rawCss, options)
  }

  class TemplateHandler {
    +templateHandler(rawWxml, options)
  }

  CreateContextResult ..> RuntimeState : holds
  CoreContextModule ..> CreateContextResult : creates
  CoreContextModule ..> JsHandler : wraps_and_caches_options
  CoreContextModule ..> StyleHandler : wraps_and_caches_options
  CoreContextModule ..> TemplateHandler : wraps_and_caches_options
Loading

File-Level Changes

Change Details Files
Refactor Vite generateBundle hot-path into reusable bundle-state snapshot API with more accurate JS dependency tracking.
  • Extract entry classification, hashing, dirty detection, and JS entry collection into bundlers/vite/bundle-state.ts with BundleSnapshot and BundleBuildState types.
  • Replace inline dirty-entry computation in generate-bundle.ts with buildBundleSnapshot/updateBundleBuildState, including new dependentsByLinkedFile map to re-queue JS entries when linked modules change.
  • Use precomputed snapshot.entries/source strings, jsEntries, and linkedImpactsByEntry in generateBundle to avoid recomputing sources and linked impact signatures; standardize debug logging on snapshot.changedByType.
packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts
packages/weapp-tailwindcss/src/bundlers/vite/bundle-state.ts
packages/weapp-tailwindcss/test/bundlers/vite-bundle-state.unit.test.ts
Cache and reuse style/template handler option objects across bundlers to reduce object churn and downstream hashing/fingerprinting.
  • Add per-file CSS handler options caches in Vite generate bundle, Webpack v4/v5 asset hooks, and uni-app-x Vite plugin; reuse cached isMainChunk/postcssOptions/majorVersion objects between runs.
  • Introduce default template handler options objects (runtimeSet, jsHandler) in core context, Webpack hooks, and gulp plugin, with reuse as long as runtimeSet is stable.
  • Add tests ensuring handler option object identity is reused for repeated CSS/HTML assets for Vite, Webpack v4/v5, gulp, and uni-app-x paths.
packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts
packages/weapp-tailwindcss/src/bundlers/webpack/BaseUnifiedPlugin/v4-assets.ts
packages/weapp-tailwindcss/src/bundlers/webpack/BaseUnifiedPlugin/v5-assets.ts
packages/weapp-tailwindcss/src/uni-app-x/vite.ts
packages/weapp-tailwindcss/src/bundlers/gulp/index.ts
packages/weapp-tailwindcss/src/core.ts
packages/weapp-tailwindcss/test/bundlers/vite-plugin.bundle.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/webpack.v4.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/webpack.v5.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/gulp.unit.test.ts
packages/weapp-tailwindcss/test/bundlers/vite-plugin.uni-app-x.unit.test.ts
packages/weapp-tailwindcss/test/core.transform-wxss-options.unit.test.ts
Improve compiler context cache key computation and reuse across runtime scopes while keeping project isolation.
  • Introduce WeakMap- and Map-based memoization layers for compiler context cache keys keyed by runtime scope serialization, avoiding repeated normalize+hash work for the same options/runtime environment.
  • Factor runtime cache scope derivation to respect explicit tailwindcssBasedir (disabling caller-location probe) and broaden environment inputs (INIT_CWD, UNI_* envs).
  • Refine normalization of Set/Map values using precomputed sort keys instead of JSON-stringify comparator and add tests for context reuse when tailwindcssBasedir is explicit.
  • Ensure getRuntimeCacheScope only includes caller in key when no explicit or env-based basedir is present.
packages/weapp-tailwindcss/src/context/compiler-context-cache.ts
packages/weapp-tailwindcss/test/context-cache.test.ts
Optimize JS literal handling and Babel processing to avoid redundant class transformation work.
  • Introduce CandidatePlan cache in js/handlers.ts to memoize per-candidate class resolution (decision + replacement) within a literal, avoiding repeat calls to resolveClassNameTransformWithResult and replacement computation.
  • Adjust replaceHandleValue to use candidate plans and maintain stability for repeated escaped runtime-set hits; add targeted regression test for repeated candidates in the same template literal.
  • Update js/babel/process.ts to precompute separate handler option objects for StringLiteral vs TemplateElement paths, avoiding object spread per node and enforcing consistent needEscaped semantics.
packages/weapp-tailwindcss/src/js/handlers.ts
packages/weapp-tailwindcss/src/js/babel/process.ts
packages/weapp-tailwindcss/test/js/handlers-branches.test.ts
packages/weapp-tailwindcss/test/js/handlers-stale-fallback-regression.test.ts
Reduce lightningcss handler fixed costs and ensure stable behavior with/without override options.
  • Refactor createLightningcssStyleHandler to precompute a base PreparedLightningcssRuntime (options, visitor, specificity replacer) and only rebuild when non-empty override options are provided.
  • Ensure that empty or omitted overrideOptions produce identical output (code, map, warnings), with new regression test for stability.
  • Keep prepareStyleOptions as the single source of truth for option normalization while reusing visitor/replaceSpecificity across calls.
packages/weapp-tailwindcss/src/lightningcss/style-handler.ts
packages/weapp-tailwindcss/test/lightningcss/style-handler.test.ts
Tighten Tailwind runtime cache by caching config stat signatures per event loop and on invalidation.
  • Add a per-config-path runtimeConfigSignatureCache with a timer-based clearing strategy to reuse fs.statSync results only within the same event-loop turn, then drop them to detect later file changes.
  • Ensure invalidateRuntimeClassSet also clears the corresponding config-path signature entry, forcing re-stat on next access.
  • Add tests validating signature reuse in a single tick, re-stat across ticks, and immediate re-stat after explicit invalidation.
packages/weapp-tailwindcss/src/tailwindcss/runtime/cache.ts
packages/weapp-tailwindcss/test/tailwindcss/runtime-cache.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我这边发现了 8 个问题,并给出了一些高层面的反馈:

  • CSS 处理器选项的缓存逻辑(Vite bundle-state、webpack v4/v5 hooks、gulp、uni-app-x)现在在多个位置被以非常相似的形式重复实现;可以考虑抽取一个共享的 helper 或 factory,把 cache-key 构造和选项构建集中到一起,以减少偏移并简化后续修改。
给 AI Agents 的提示
Please address the comments from this code review:

## Overall Comments
- The CSS handler options caching logic (Vite bundle-state, webpack v4/v5 hooks, gulp, uni-app-x) is now duplicated in several places with very similar shapes; consider extracting a shared helper or factory to centralize the cache-key construction and option building to reduce drift and ease future changes.

## Individual Comments

### Comment 1
<location path="packages/weapp-tailwindcss/src/context/compiler-context-cache.ts" line_range="148-157" />
<code_context>
+  return serializeNormalizedValue(normalizeOptionsValue(getRuntimeCacheScope(opts)))
+}
+
+function getCompilerContextKeyCacheStore(opts?: UserDefinedOptions) {
+  if (!opts) {
+    return compilerContextKeyCacheWithoutOptions
+  }
+
+  let store = compilerContextKeyCacheByOptions.get(opts)
+  if (!store) {
+    store = new Map<string, string | undefined>()
+    compilerContextKeyCacheByOptions.set(opts, store)
+  }
+  return store
+}
+
</code_context>
<issue_to_address>
**question (bug_risk):** Per-options cache store keyed only by runtime scope may ignore later option mutations.

Because the inner Map is keyed only by `runtimeCacheScopeKey`, any mutation of a reused `opts` object (e.g., changing `tailwindcssBasedir`) won’t affect the cache key. Calls with mutated options will still reuse the old `{ options, runtime }` entry. If callers are allowed to mutate `opts`, consider either treating `opts` as immutable and documenting that, or including a lightweight options fingerprint in the key to prevent stale cache hits.
</issue_to_address>

### Comment 2
<location path="packages/weapp-tailwindcss/test/bundlers/vite-bundle-state.unit.test.ts" line_range="23-32" />
<code_context>
+describe('bundlers/vite bundle state', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for unchanged HTML still being scheduled for processing on subsequent snapshots

The updated `buildBundleSnapshot` always enqueues HTML for processing after the first run, even if the source hash is unchanged (to support the uni-app/HBuilderX watch behavior). The current tests cover JS dependency requeueing and stale entry cleanup, but they don’t verify this HTML behavior.

Please add a test that runs two snapshots with unchanged HTML and JS/CSS, and asserts that:
- `changedByType.html` is empty on the second snapshot, and
- `processFiles.html` still includes the HTML entry.

This will lock in the intended watch behavior and guard against regressions in future refactors.

Suggested implementation:

```typescript
describe('bundlers/vite bundle state', () => {
  it('re-queues html entries for processing even when unchanged between snapshots', () => {
    const opts = createOptions()
    const state = createBundleBuildState()
    const outDir = '/project/dist'

    const bundleAssets = {
      'pages/index/index.wxml': {
        ...createRollupAsset('<view class="foo">hello</view>'),
        fileName: 'pages/index/index.wxml',
      },
      'pages/index/index.js': {
        ...createRollupChunk("import './index.wxss'; console.log('index');"),
        fileName: 'pages/index/index.js',
      },
      'pages/index/index.wxss': {
        ...createRollupAsset('.foo { color: red; }'),
        fileName: 'pages/index/index.wxss',
      },
    }

    const firstSnapshot = buildBundleSnapshot(
      bundleAssets,
      state,
      opts,
      outDir,
    )

    const secondSnapshot = buildBundleSnapshot(
      bundleAssets,
      state,
      opts,
      outDir,
    )

    expect(secondSnapshot.changedByType.html).toEqual([])

    expect(secondSnapshot.processFiles.html).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          file: 'pages/index/index.wxml',
        }),
      ]),
    )
  })

```

Depending on the existing helpers and snapshot shape in this file, you may need to:

1. Adjust the `createRollupChunk` import/usage:
   - If `createRollupChunk` is not already imported in this test file, add an import consistent with other tests (likely from the same helper module as `createRollupAsset`).
2. Align with the actual `buildBundleSnapshot` signature:
   - If other tests pass parameters in a different order or wrapped in an options object, mirror that exact calling convention for both `firstSnapshot` and `secondSnapshot`.
3. Match the actual structure of `processFiles.html`:
   - If other tests assert on `processFiles.html` using a different property name (e.g. `id`, `filename`, `fileName`, or `sourcePath` instead of `file`), update the `expect.objectContaining({ file: 'pages/index/index.wxml' })` accordingly to match the real structure.
4. If CSS files use `.css` instead of `.wxss` in your test suite, adjust the `fileName` and import path to match the existing convention in this file.
</issue_to_address>

### Comment 3
<location path="packages/weapp-tailwindcss/test/core.transform-wxss-options.unit.test.ts" line_range="148-157" />
<code_context>
+describe('core transformJs option reuse', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Extend transformJs/transformWxml tests to cover mixed override cases

The added tests cover default-option reuse and main pass-through paths well. To fully exercise the new merging logic, consider adding tests for:

1) `transformJs` with both `runtimeSet` and explicit handler options:
   - Call `transformJs({ runtimeSet, tailwindcssMajorVersion: 3, generateMap: true })` and assert that:
     - `runtimeSet` is omitted from the third arg to `jsHandler`,
     - `tailwindcssMajorVersion: 3` and `generateMap: true` are preserved,
     - no default major-version inference is applied on top.

2) `transformWxml` with non-empty `ITemplateHandlerOptions`:
   - Use a custom `whitelist` or `escapeMap` and assert that:
     - caller overrides are preserved in the options to `templateHandler`,
     - `runtimeSet` and the wrapped `jsHandler` are still present.

These will help ensure the caching/merge logic doesn’t drop or override user options in more complex cases.

Suggested implementation:

```typescript
describe('core transformJs option reuse', () => {
  beforeEach(() => {
    styleHandler.mockClear()
    templateHandler.mockClear()
    jsHandler.mockClear()
    refreshTailwindcssPatcher.mockClear()
    ensureRuntimeClassSet.mockClear()
    setupPatchRecorder.mockClear()
    getCompilerContext.mockClear()
  })

  it('passes explicit js handler options through transformJs while omitting runtimeSet from jsHandler options', async () => {
    const runtimeSet = new Set<string>()

    await transformJs('const foo = "bar"', {
      runtimeSet,
      tailwindcssMajorVersion: 3,
      generateMap: true,
    })

    expect(jsHandler).toHaveBeenCalledTimes(1)

    const handlerCall = jsHandler.mock.calls[0]
    const handlerOptions = handlerCall?.[2]

    // runtimeSet should be consumed by the core layer and not forwarded to jsHandler
    expect(handlerOptions.runtimeSet).toBeUndefined()

    // Explicit options should be preserved as-is
    expect(handlerOptions.tailwindcssMajorVersion).toBe(3)
    expect(handlerOptions.generateMap).toBe(true)

    // No default major-version inference should override explicit value
    // (i.e. nothing like "fallback to v2 or v3" should have changed it)
    expect(handlerOptions.tailwindcssMajorVersion).toBe(3)
  })

  it('preserves template handler overrides while injecting runtimeSet and wrapped jsHandler in transformWxml', async () => {
    const runtimeSet = new Set<string>()
    const whitelist = ['view', 'text']
    const escapeMap = {
      '&': '&amp;',
      '<': '&lt;',
    }

    await transformWxml('<view class="text-[12px]"></view>', {
      runtimeSet,
      whitelist,
      escapeMap,
    })

    expect(templateHandler).toHaveBeenCalledTimes(1)

    const handlerCall = templateHandler.mock.calls[0]
    const handlerOptions = handlerCall?.[1]

    // Caller overrides should be preserved
    expect(handlerOptions.whitelist).toEqual(whitelist)
    expect(handlerOptions.escapeMap).toEqual(escapeMap)

    // runtimeSet should still be present for the templateHandler
    expect(handlerOptions.runtimeSet).toBe(runtimeSet)

    // jsHandler should be a wrapped function provided by core so WXML
    // can drive JS transformation when needed.
    expect(typeof handlerOptions.jsHandler).toBe('function')
  })
})

```

1. Ensure `transformJs` and `transformWxml` are imported in this test file (if they are not already), e.g.:
   `import { transformJs, transformWxml } from '../../src/core'` (adjust the path to match the existing imports in this file).
2. If this test file uses a shared factory/context instead of directly calling `transformJs`/`transformWxml`, adapt the calls to go through that context (e.g. `const ctx = await getCompilerContext(...); await ctx.transformJs(...); await ctx.transformWxml(...);`) while keeping the same expectations on `jsHandler.mock.calls` and `templateHandler.mock.calls`.
3. If `ITemplateHandlerOptions` in your codebase uses different property names than `whitelist`/`escapeMap`, align the test option names with the actual interface.
</issue_to_address>

### Comment 4
<location path="packages/weapp-tailwindcss/test/js/handlers-branches.test.ts" line_range="156-164" />
<code_context>
     expect(token?.value).toBe('w-_b100px_B w-_b100px_B')
   })

+  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
+    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
+    const token = replaceHandleValue(quasi, {
+      escapeMap: MappingChars2String,
+      classNameSet: new Set(['gap-_b20px_B']),
+      needEscaped: false,
+    })
+
+    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
+  })
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add explicit coverage for the new needEscaped branching in processUpdatedSource

Since `needEscaped` is now derived differently for string literals vs template elements, it would help to add a focused test that:
- builds an AST containing both a `StringLiteral` and a `TemplateElement` with the same class value,
- calls `processUpdatedSource` without passing `needEscaped` in options,
- asserts that the string literal path yields `JsToken.value` with `needEscaped === true`, and the template element path yields `needEscaped === false`.

This directly verifies the new branching behavior and guards against subtle regressions in `needEscaped` handling.

Suggested implementation:

```typescript
  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
    const token = replaceHandleValue(quasi, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      needEscaped: false,
    })

    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
  })

  it('derives needEscaped differently for string literals and template elements', () => {
    const stringLiteral = getLiteralPath('const s = "gap-[20px]"', 'StringLiteral')
    const templateElement = getLiteralPath('const tpl = `gap-[20px]`', 'TemplateElement')

    const stringToken = processUpdatedSource(stringLiteral, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    const templateToken = processUpdatedSource(templateElement, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    expect(stringToken?.value).toBe('gap-_b20px_B')
    expect(stringToken?.needEscaped).toBe(true)

    expect(templateToken?.value).toBe('gap-_b20px_B')
    expect(templateToken?.needEscaped).toBe(false)
  })

```

1. Ensure `processUpdatedSource` is imported at the top of `handlers-branches.test.ts` from the same module where `replaceHandleValue` is imported (often something like `../../src/js/handlers` or similar in this repo).
2. If `JsToken` is a typed object and the tests elsewhere use type assertions/imports, you may also want to import its type to keep TypeScript happy (e.g., `import type { JsToken } from '...'`), though this is optional in the test if you rely on type inference.
</issue_to_address>

### Comment 5
<location path="packages/weapp-tailwindcss/src/bundlers/vite/bundle-state.ts" line_range="108" />
<code_context>
+  }
+}
+
+export function buildBundleSnapshot(
+  bundle: Record<string, OutputAsset | OutputChunk>,
+  opts: InternalUserDefinedOptions,
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring `buildBundleSnapshot` into smaller helper functions so each concern (base snapshot computation and linked impacts) is handled in a focused, testable unit while preserving the current API and behavior.

You can keep the new module + types but reduce complexity by decomposing `buildBundleSnapshot` into smaller helpers with narrow responsibilities, while keeping the external API identical.

Right now `buildBundleSnapshot` is doing:

- classification
- hashing + change detection
- process set computation
- JS entry collection
- snapshot entry building
- linked impact propagation

All fused in one ~80-line routine.

A small refactor could look like this:

```ts
function computeBaseSnapshot(
  bundle: Record<string, OutputAsset | OutputChunk>,
  opts: InternalUserDefinedOptions,
  outDir: string,
  state: BundleBuildState,
  forceAll: boolean,
): {
  entries: BundleStateEntry[]
  sourceHashByFile: Map<string, string>
  changedByType: Record<EntryType, Set<string>>
  processFiles: ProcessFileSets
  jsEntries: Map<string, OutputEntry>
} {
  const sourceHashByFile = new Map<string, string>()
  const changedByType = createChangedByType()
  const processFiles = createProcessFiles()
  const jsEntries = new Map<string, OutputEntry>()
  const entries: BundleStateEntry[] = []
  const firstRun = state.linkedByEntry.size === 0

  for (const [file, output] of Object.entries(bundle)) {
    const type = classifyBundleEntry(file, opts)
    const source = readEntrySource(output)
    const hash = opts.cache.computeHash(source)
    sourceHashByFile.set(file, hash)

    const previousHash = state.sourceHashByFile.get(file)
    const changed = previousHash == null || previousHash !== hash
    if (changed) {
      changedByType[type].add(file)
    }

    if (forceAll || firstRun) {
      markProcessFile(type, file, processFiles)
    } else if (type === 'html') {
      processFiles.html.add(file)
    } else if (changed && (type === 'js' || type === 'css')) {
      processFiles[type].add(file)
    }

    collectJsEntries(file, output, outDir, jsEntries)
    entries.push({ file, output, source, type })
  }

  return { entries, sourceHashByFile, changedByType, processFiles, jsEntries }
}

function computeLinkedImpacts(
  changedJs: Set<string>,
  state: BundleBuildState,
  processFiles: ProcessFileSets,
): Map<string, Set<string>> {
  const linkedImpactsByEntry = new Map<string, Set<string>>()

  for (const changedFile of changedJs) {
    const dependents = state.dependentsByLinkedFile.get(changedFile)
    if (!dependents) continue

    for (const entryFile of dependents) {
      processFiles.js.add(entryFile)
      let impacts = linkedImpactsByEntry.get(entryFile)
      if (!impacts) {
        impacts = new Set<string>()
        linkedImpactsByEntry.set(entryFile, impacts)
      }
      impacts.add(changedFile)
    }
  }

  return linkedImpactsByEntry
}
```

Then `buildBundleSnapshot` becomes simpler and easier to reason about:

```ts
export function buildBundleSnapshot(
  bundle: Record<string, OutputAsset | OutputChunk>,
  opts: InternalUserDefinedOptions,
  outDir: string,
  state: BundleBuildState,
  forceAll = false,
): BundleSnapshot {
  const {
    entries,
    sourceHashByFile,
    changedByType,
    processFiles,
    jsEntries,
  } = computeBaseSnapshot(bundle, opts, outDir, state, forceAll)

  const firstRun = state.linkedByEntry.size === 0
  const linkedImpactsByEntry =
    !forceAll && !firstRun
      ? computeLinkedImpacts(changedByType.js, state, processFiles)
      : new Map<string, Set<string>>()

  return {
    entries,
    jsEntries,
    sourceHashByFile,
    changedByType,
    processFiles,
    linkedImpactsByEntry,
  }
}
```

This keeps:

- `BundleSnapshot` shape unchanged,
- `BundleBuildState` unchanged,
- behavior of hashing, change detection, and linked impact computation unchanged,

but makes each concern testable and understandable in isolation, addressing the “one big routine” concern without reverting your structural changes.
</issue_to_address>

### Comment 6
<location path="packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts" line_range="293" />
<code_context>

-    for (const [file, originalSource] of entries) {
-      const type = classifyEntry(file, opts)
+    for (const entry of snapshot.entries) {
+      const { file, output: originalSource, source: originalEntrySource, type } = entry

</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the `snapshot.entries` wrapper with simpler snapshot maps and iterating the original `bundle` to reduce indirection while preserving the new state and dirty-tracking behavior.

You can reduce the new indirection without losing the reuse/state-management improvements from `bundle-state.ts`.

The core pain point is `snapshot.entries`: it wraps `(file, output, source, type)` into an object that is immediately destructured, while the hook already has `bundle` and only needs a couple of derived properties. You can keep all the new capabilities (dirty detection, process sets, jsEntries, linked impacts, hashes) and simplify the hook by shifting `entries` into more focused maps and iterating the original `bundle` again.

### 1. Replace `snapshot.entries` with focused maps

In `bundle-state.ts`, instead of:

```ts
export interface BundleSnapshot {
  // ...
  entries: Array<BundleStateEntry>
  jsEntries: Map<string, OutputEntry>
  processFiles: ProcessFileSets
  changedByType: Record<EntryType, Set<string>>
  linkedImpactsByEntry: Map<string, Set<string>>
  sourceHashByFile: Map<string, string>
}
```

shape it as:

```ts
export interface BundleSnapshot {
  jsEntries: Map<string, OutputEntry>
  processFiles: ProcessFileSets
  changedByType: Record<EntryType, Set<string>>
  linkedImpactsByEntry: Map<string, Set<string>>
  sourceHashByFile: Map<string, string>
  entrySources: Map<string, string> // file -> original entry source
  entryTypes: Map<string, EntryType> // file -> 'html' | 'css' | 'js' | 'other'
}
```

and have `buildBundleSnapshot` fill `entrySources` and `entryTypes` while doing the hashing and classification it already does today.

### 2. Iterate the original bundle in the hook

In the hook, you can then remove the `BundleStateEntry` wrapper usage and go back to a simple `Object.entries(bundle)` loop, but still reuse the precomputed data from the snapshot:

```ts
// before
for (const entry of snapshot.entries) {
  const { file, output: originalSource, source: originalEntrySource, type } = entry
  // ...
}

// after
for (const [file, originalSource] of Object.entries(bundle)) {
  const type = snapshot.entryTypes.get(file) ?? 'other'
  const originalEntrySource =
    snapshot.entrySources.get(file) ??
    (originalSource.type === 'chunk'
      ? originalSource.code
      : originalSource.source.toString())

  if (type === 'html' && originalSource.type === 'asset') {
    metrics.html.total++
    if (!processFiles.html.has(file)) continue

    const rawSource = originalEntrySource
    // ... unchanged cache logic
  }

  if (type === 'css' && originalSource.type === 'asset') {
    metrics.css.total++
    const rawSource = originalEntrySource
    // ... unchanged cache/styleHandler logic
  }

  if (type === 'js') {
    metrics.js.total++
    const shouldTransformJs = processFiles.js.has(file)
    // use originalEntrySource where you now use originalSource.code / .toString()
  }
}
```

This keeps:

- `buildBundleSnapshot` as the single place for classification/hashing.
- `updateBundleBuildState` as the single place for state evolution.
- `jsEntries`, `processFiles`, `changedByType`, `linkedImpactsByEntry`, `sourceHashByFile` unchanged.

But the hook is once again expressed in terms of `bundle` plus a few snapshot lookups, instead of a bespoke `BundleStateEntry` type, which makes the data flow easier to follow and reduces coupling to `bundle-state.ts` internals.
</issue_to_address>

### Comment 7
<location path="packages/weapp-tailwindcss/src/context/compiler-context-cache.ts" line_range="161" />
<code_context>
+  return store
+}
+
+interface ComparableNormalizedValue {
+  normalized: NormalizedOptionsValue
+  sortKey: string
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining the small helper abstractions (`ComparableNormalizedValue`/`createComparableNormalizedValue` and `getRuntimeCacheScopeValue`) directly at their call sites to keep the control flow flatter and easier to follow without changing behavior.

You can trim a couple of abstraction layers without changing behavior or the new caching strategy.

### 1. Inline `ComparableNormalizedValue` / `createComparableNormalizedValue`

The only use of `ComparableNormalizedValue` is to pair `normalized` with a `sortKey`. You can avoid the extra type + helper and do this locally where needed:

```ts
// remove:
// interface ComparableNormalizedValue { ... }
// function createComparableNormalizedValue(...) { ... }

// Set
if (rawValue instanceof Set) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue, element => {
      const normalized = normalizeOptionsValue(element, stack)
      return {
        normalized,
        sortKey: JSON.stringify(normalized),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Set',
      value: normalizedEntries.map(entry => entry.normalized),
    }
  }) as Record<string, NormalizedOptionsValue>
}

// Map
if (rawValue instanceof Map) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue.entries()).map(([key, entryValue]) => {
      const normalizedKey = normalizeOptionsValue(key, stack)
      return {
        key: normalizedKey,
        sortKey: JSON.stringify(normalizedKey),
        value: normalizeOptionsValue(entryValue, stack),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Map',
      value: normalizedEntries.map(entry => [entry.key, entry.value]),
    }
  }) as Record<string, NormalizedOptionsValue>
}
```

This keeps the precomputed sort keys (same perf characteristic) but removes a named type plus a helper that future readers need to chase.

### 2. Inline `getRuntimeCacheScopeValue` at the call site

`getRuntimeCacheScopeValue` is a thin wrapper that makes the call chain deeper. You can keep `getRuntimeCacheScope(opts)` as-is and inline the outer wrapper:

```ts
// remove:
// function getRuntimeCacheScopeValue(opts?: UserDefinedOptions) {
//   return {
//     options: opts ?? {},
//     runtime: getRuntimeCacheScope(opts),
//   }
// }

export function createCompilerContextCacheKey(opts?: UserDefinedOptions): string | undefined {
  try {
    const runtimeCacheScopeKey = createRuntimeCacheScopeKey(opts)
    const keyStore = getCompilerContextKeyCacheStore(opts)
    const cached = keyStore.get(runtimeCacheScopeKey)
    if (cached !== undefined) {
      return cached
    }

    const normalized = normalizeOptionsValue({
      options: opts ?? {},
      runtime: getRuntimeCacheScope(opts),
    })
    const cacheKey = md5Hash(JSON.stringify(normalized))
    keyStore.set(runtimeCacheScopeKey, cacheKey)
    return cacheKey
  }
  catch (error) {
    logger.debug('skip compiler context cache: %O', error)
    return undefined
  }
}
```

This preserves the two-tier cache (`WeakMap` vs `Map`) and runtime scope behavior, but the flow from `createCompilerContextCacheKey` is more straightforward and easier to debug.
</issue_to_address>

### Comment 8
<location path="packages/weapp-tailwindcss/src/core.ts" line_range="63" />
<code_context>
     return resolvedOptions
   }

+  function resolveTransformJsOptions(options?: RuntimeJsTransformOptions): CreateJsHandlerOptions | undefined {
+    if (!options) {
+      const majorVersion = runtimeState.twPatcher.majorVersion
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting small helper functions for default JS and template handler options to avoid duplicated logic and simplify the branching in the transform option resolvers.

You can keep the new behavior (caching + runtime-awareness) but reduce branching and duplication by extracting small helpers.

### 1. Simplify `resolveTransformJsOptions`

Right now `resolveTransformJsOptions` duplicates the “get default by majorVersion” logic and has one extra special-case branch for `runtimeSet`. You can centralize the default logic and always strip `runtimeSet` in one place:

```ts
function getDefaultJsHandlerOptions(): CreateJsHandlerOptions | undefined {
  const majorVersion = runtimeState.twPatcher.majorVersion
  if (typeof majorVersion !== 'number') {
    return undefined
  }

  let cached = defaultJsHandlerOptionsCache.get(majorVersion)
  if (!cached) {
    cached = { tailwindcssMajorVersion: majorVersion }
    defaultJsHandlerOptionsCache.set(majorVersion, cached)
  }
  return cached
}

function resolveTransformJsOptions(
  options?: RuntimeJsTransformOptions,
): CreateJsHandlerOptions | undefined {
  if (!options) {
    return getDefaultJsHandlerOptions()
  }

  const { runtimeSet: _runtimeSet, ...handlerOptions } = options

  if (Object.keys(handlerOptions).length === 0) {
    return getDefaultJsHandlerOptions()
  }

  if (typeof handlerOptions.tailwindcssMajorVersion === 'number') {
    return handlerOptions
  }

  return withRuntimeTailwindMajorVersion(handlerOptions)
}
```

This keeps:

- caching by `majorVersion`
- support for `runtimeSet` on the options object
- “empty options” → default behavior

while reducing the number of decision paths and duplicated code.

### 2. Reduce duplication in `resolveTransformWxmlOptions`

You can keep memoization but wrap it in a tiny “default template options” helper, so `resolveTransformWxmlOptions` only has one branch:

```ts
function getDefaultTemplateHandlerOptions(): ITemplateHandlerOptions {
  if (!cachedDefaultTemplateHandlerOptions || cachedDefaultTemplateRuntimeSet !== runtimeSet) {
    cachedDefaultTemplateRuntimeSet = runtimeSet
    cachedDefaultTemplateHandlerOptions = {
      runtimeSet,
      jsHandler: runtimeAwareTemplateJsHandler,
    }
  }
  return cachedDefaultTemplateHandlerOptions
}

function resolveTransformWxmlOptions(options?: ITemplateHandlerOptions) {
  const base = getDefaultTemplateHandlerOptions()
  return options
    ? defuOverrideArray(options, base)
    : base
}
```

This preserves:

- caching by `runtimeSet`
- `jsHandler` indirection with `runtimeAwareTemplateJsHandler`
- `defuOverrideArray` merging

but makes the control flow for template transforms easier to follow.
</issue_to_address>

Sourcery 对开源项目免费使用 - 如果你觉得这些 Review 有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的 Review。
Original comment in English

Hey - I've found 8 issues, and left some high level feedback:

  • The CSS handler options caching logic (Vite bundle-state, webpack v4/v5 hooks, gulp, uni-app-x) is now duplicated in several places with very similar shapes; consider extracting a shared helper or factory to centralize the cache-key construction and option building to reduce drift and ease future changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The CSS handler options caching logic (Vite bundle-state, webpack v4/v5 hooks, gulp, uni-app-x) is now duplicated in several places with very similar shapes; consider extracting a shared helper or factory to centralize the cache-key construction and option building to reduce drift and ease future changes.

## Individual Comments

### Comment 1
<location path="packages/weapp-tailwindcss/src/context/compiler-context-cache.ts" line_range="148-157" />
<code_context>
+  return serializeNormalizedValue(normalizeOptionsValue(getRuntimeCacheScope(opts)))
+}
+
+function getCompilerContextKeyCacheStore(opts?: UserDefinedOptions) {
+  if (!opts) {
+    return compilerContextKeyCacheWithoutOptions
+  }
+
+  let store = compilerContextKeyCacheByOptions.get(opts)
+  if (!store) {
+    store = new Map<string, string | undefined>()
+    compilerContextKeyCacheByOptions.set(opts, store)
+  }
+  return store
+}
+
</code_context>
<issue_to_address>
**question (bug_risk):** Per-options cache store keyed only by runtime scope may ignore later option mutations.

Because the inner Map is keyed only by `runtimeCacheScopeKey`, any mutation of a reused `opts` object (e.g., changing `tailwindcssBasedir`) won’t affect the cache key. Calls with mutated options will still reuse the old `{ options, runtime }` entry. If callers are allowed to mutate `opts`, consider either treating `opts` as immutable and documenting that, or including a lightweight options fingerprint in the key to prevent stale cache hits.
</issue_to_address>

### Comment 2
<location path="packages/weapp-tailwindcss/test/bundlers/vite-bundle-state.unit.test.ts" line_range="23-32" />
<code_context>
+describe('bundlers/vite bundle state', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for unchanged HTML still being scheduled for processing on subsequent snapshots

The updated `buildBundleSnapshot` always enqueues HTML for processing after the first run, even if the source hash is unchanged (to support the uni-app/HBuilderX watch behavior). The current tests cover JS dependency requeueing and stale entry cleanup, but they don’t verify this HTML behavior.

Please add a test that runs two snapshots with unchanged HTML and JS/CSS, and asserts that:
- `changedByType.html` is empty on the second snapshot, and
- `processFiles.html` still includes the HTML entry.

This will lock in the intended watch behavior and guard against regressions in future refactors.

Suggested implementation:

```typescript
describe('bundlers/vite bundle state', () => {
  it('re-queues html entries for processing even when unchanged between snapshots', () => {
    const opts = createOptions()
    const state = createBundleBuildState()
    const outDir = '/project/dist'

    const bundleAssets = {
      'pages/index/index.wxml': {
        ...createRollupAsset('<view class="foo">hello</view>'),
        fileName: 'pages/index/index.wxml',
      },
      'pages/index/index.js': {
        ...createRollupChunk("import './index.wxss'; console.log('index');"),
        fileName: 'pages/index/index.js',
      },
      'pages/index/index.wxss': {
        ...createRollupAsset('.foo { color: red; }'),
        fileName: 'pages/index/index.wxss',
      },
    }

    const firstSnapshot = buildBundleSnapshot(
      bundleAssets,
      state,
      opts,
      outDir,
    )

    const secondSnapshot = buildBundleSnapshot(
      bundleAssets,
      state,
      opts,
      outDir,
    )

    expect(secondSnapshot.changedByType.html).toEqual([])

    expect(secondSnapshot.processFiles.html).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          file: 'pages/index/index.wxml',
        }),
      ]),
    )
  })

```

Depending on the existing helpers and snapshot shape in this file, you may need to:

1. Adjust the `createRollupChunk` import/usage:
   - If `createRollupChunk` is not already imported in this test file, add an import consistent with other tests (likely from the same helper module as `createRollupAsset`).
2. Align with the actual `buildBundleSnapshot` signature:
   - If other tests pass parameters in a different order or wrapped in an options object, mirror that exact calling convention for both `firstSnapshot` and `secondSnapshot`.
3. Match the actual structure of `processFiles.html`:
   - If other tests assert on `processFiles.html` using a different property name (e.g. `id`, `filename`, `fileName`, or `sourcePath` instead of `file`), update the `expect.objectContaining({ file: 'pages/index/index.wxml' })` accordingly to match the real structure.
4. If CSS files use `.css` instead of `.wxss` in your test suite, adjust the `fileName` and import path to match the existing convention in this file.
</issue_to_address>

### Comment 3
<location path="packages/weapp-tailwindcss/test/core.transform-wxss-options.unit.test.ts" line_range="148-157" />
<code_context>
+describe('core transformJs option reuse', () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Extend transformJs/transformWxml tests to cover mixed override cases

The added tests cover default-option reuse and main pass-through paths well. To fully exercise the new merging logic, consider adding tests for:

1) `transformJs` with both `runtimeSet` and explicit handler options:
   - Call `transformJs({ runtimeSet, tailwindcssMajorVersion: 3, generateMap: true })` and assert that:
     - `runtimeSet` is omitted from the third arg to `jsHandler`,
     - `tailwindcssMajorVersion: 3` and `generateMap: true` are preserved,
     - no default major-version inference is applied on top.

2) `transformWxml` with non-empty `ITemplateHandlerOptions`:
   - Use a custom `whitelist` or `escapeMap` and assert that:
     - caller overrides are preserved in the options to `templateHandler`,
     - `runtimeSet` and the wrapped `jsHandler` are still present.

These will help ensure the caching/merge logic doesn’t drop or override user options in more complex cases.

Suggested implementation:

```typescript
describe('core transformJs option reuse', () => {
  beforeEach(() => {
    styleHandler.mockClear()
    templateHandler.mockClear()
    jsHandler.mockClear()
    refreshTailwindcssPatcher.mockClear()
    ensureRuntimeClassSet.mockClear()
    setupPatchRecorder.mockClear()
    getCompilerContext.mockClear()
  })

  it('passes explicit js handler options through transformJs while omitting runtimeSet from jsHandler options', async () => {
    const runtimeSet = new Set<string>()

    await transformJs('const foo = "bar"', {
      runtimeSet,
      tailwindcssMajorVersion: 3,
      generateMap: true,
    })

    expect(jsHandler).toHaveBeenCalledTimes(1)

    const handlerCall = jsHandler.mock.calls[0]
    const handlerOptions = handlerCall?.[2]

    // runtimeSet should be consumed by the core layer and not forwarded to jsHandler
    expect(handlerOptions.runtimeSet).toBeUndefined()

    // Explicit options should be preserved as-is
    expect(handlerOptions.tailwindcssMajorVersion).toBe(3)
    expect(handlerOptions.generateMap).toBe(true)

    // No default major-version inference should override explicit value
    // (i.e. nothing like "fallback to v2 or v3" should have changed it)
    expect(handlerOptions.tailwindcssMajorVersion).toBe(3)
  })

  it('preserves template handler overrides while injecting runtimeSet and wrapped jsHandler in transformWxml', async () => {
    const runtimeSet = new Set<string>()
    const whitelist = ['view', 'text']
    const escapeMap = {
      '&': '&amp;',
      '<': '&lt;',
    }

    await transformWxml('<view class="text-[12px]"></view>', {
      runtimeSet,
      whitelist,
      escapeMap,
    })

    expect(templateHandler).toHaveBeenCalledTimes(1)

    const handlerCall = templateHandler.mock.calls[0]
    const handlerOptions = handlerCall?.[1]

    // Caller overrides should be preserved
    expect(handlerOptions.whitelist).toEqual(whitelist)
    expect(handlerOptions.escapeMap).toEqual(escapeMap)

    // runtimeSet should still be present for the templateHandler
    expect(handlerOptions.runtimeSet).toBe(runtimeSet)

    // jsHandler should be a wrapped function provided by core so WXML
    // can drive JS transformation when needed.
    expect(typeof handlerOptions.jsHandler).toBe('function')
  })
})

```

1. Ensure `transformJs` and `transformWxml` are imported in this test file (if they are not already), e.g.:
   `import { transformJs, transformWxml } from '../../src/core'` (adjust the path to match the existing imports in this file).
2. If this test file uses a shared factory/context instead of directly calling `transformJs`/`transformWxml`, adapt the calls to go through that context (e.g. `const ctx = await getCompilerContext(...); await ctx.transformJs(...); await ctx.transformWxml(...);`) while keeping the same expectations on `jsHandler.mock.calls` and `templateHandler.mock.calls`.
3. If `ITemplateHandlerOptions` in your codebase uses different property names than `whitelist`/`escapeMap`, align the test option names with the actual interface.
</issue_to_address>

### Comment 4
<location path="packages/weapp-tailwindcss/test/js/handlers-branches.test.ts" line_range="156-164" />
<code_context>
     expect(token?.value).toBe('w-_b100px_B w-_b100px_B')
   })

+  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
+    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
+    const token = replaceHandleValue(quasi, {
+      escapeMap: MappingChars2String,
+      classNameSet: new Set(['gap-_b20px_B']),
+      needEscaped: false,
+    })
+
+    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
+  })
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add explicit coverage for the new needEscaped branching in processUpdatedSource

Since `needEscaped` is now derived differently for string literals vs template elements, it would help to add a focused test that:
- builds an AST containing both a `StringLiteral` and a `TemplateElement` with the same class value,
- calls `processUpdatedSource` without passing `needEscaped` in options,
- asserts that the string literal path yields `JsToken.value` with `needEscaped === true`, and the template element path yields `needEscaped === false`.

This directly verifies the new branching behavior and guards against subtle regressions in `needEscaped` handling.

Suggested implementation:

```typescript
  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
    const token = replaceHandleValue(quasi, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      needEscaped: false,
    })

    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
  })

  it('derives needEscaped differently for string literals and template elements', () => {
    const stringLiteral = getLiteralPath('const s = "gap-[20px]"', 'StringLiteral')
    const templateElement = getLiteralPath('const tpl = `gap-[20px]`', 'TemplateElement')

    const stringToken = processUpdatedSource(stringLiteral, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    const templateToken = processUpdatedSource(templateElement, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    expect(stringToken?.value).toBe('gap-_b20px_B')
    expect(stringToken?.needEscaped).toBe(true)

    expect(templateToken?.value).toBe('gap-_b20px_B')
    expect(templateToken?.needEscaped).toBe(false)
  })

```

1. Ensure `processUpdatedSource` is imported at the top of `handlers-branches.test.ts` from the same module where `replaceHandleValue` is imported (often something like `../../src/js/handlers` or similar in this repo).
2. If `JsToken` is a typed object and the tests elsewhere use type assertions/imports, you may also want to import its type to keep TypeScript happy (e.g., `import type { JsToken } from '...'`), though this is optional in the test if you rely on type inference.
</issue_to_address>

### Comment 5
<location path="packages/weapp-tailwindcss/src/bundlers/vite/bundle-state.ts" line_range="108" />
<code_context>
+  }
+}
+
+export function buildBundleSnapshot(
+  bundle: Record<string, OutputAsset | OutputChunk>,
+  opts: InternalUserDefinedOptions,
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring `buildBundleSnapshot` into smaller helper functions so each concern (base snapshot computation and linked impacts) is handled in a focused, testable unit while preserving the current API and behavior.

You can keep the new module + types but reduce complexity by decomposing `buildBundleSnapshot` into smaller helpers with narrow responsibilities, while keeping the external API identical.

Right now `buildBundleSnapshot` is doing:

- classification
- hashing + change detection
- process set computation
- JS entry collection
- snapshot entry building
- linked impact propagation

All fused in one ~80-line routine.

A small refactor could look like this:

```ts
function computeBaseSnapshot(
  bundle: Record<string, OutputAsset | OutputChunk>,
  opts: InternalUserDefinedOptions,
  outDir: string,
  state: BundleBuildState,
  forceAll: boolean,
): {
  entries: BundleStateEntry[]
  sourceHashByFile: Map<string, string>
  changedByType: Record<EntryType, Set<string>>
  processFiles: ProcessFileSets
  jsEntries: Map<string, OutputEntry>
} {
  const sourceHashByFile = new Map<string, string>()
  const changedByType = createChangedByType()
  const processFiles = createProcessFiles()
  const jsEntries = new Map<string, OutputEntry>()
  const entries: BundleStateEntry[] = []
  const firstRun = state.linkedByEntry.size === 0

  for (const [file, output] of Object.entries(bundle)) {
    const type = classifyBundleEntry(file, opts)
    const source = readEntrySource(output)
    const hash = opts.cache.computeHash(source)
    sourceHashByFile.set(file, hash)

    const previousHash = state.sourceHashByFile.get(file)
    const changed = previousHash == null || previousHash !== hash
    if (changed) {
      changedByType[type].add(file)
    }

    if (forceAll || firstRun) {
      markProcessFile(type, file, processFiles)
    } else if (type === 'html') {
      processFiles.html.add(file)
    } else if (changed && (type === 'js' || type === 'css')) {
      processFiles[type].add(file)
    }

    collectJsEntries(file, output, outDir, jsEntries)
    entries.push({ file, output, source, type })
  }

  return { entries, sourceHashByFile, changedByType, processFiles, jsEntries }
}

function computeLinkedImpacts(
  changedJs: Set<string>,
  state: BundleBuildState,
  processFiles: ProcessFileSets,
): Map<string, Set<string>> {
  const linkedImpactsByEntry = new Map<string, Set<string>>()

  for (const changedFile of changedJs) {
    const dependents = state.dependentsByLinkedFile.get(changedFile)
    if (!dependents) continue

    for (const entryFile of dependents) {
      processFiles.js.add(entryFile)
      let impacts = linkedImpactsByEntry.get(entryFile)
      if (!impacts) {
        impacts = new Set<string>()
        linkedImpactsByEntry.set(entryFile, impacts)
      }
      impacts.add(changedFile)
    }
  }

  return linkedImpactsByEntry
}
```

Then `buildBundleSnapshot` becomes simpler and easier to reason about:

```ts
export function buildBundleSnapshot(
  bundle: Record<string, OutputAsset | OutputChunk>,
  opts: InternalUserDefinedOptions,
  outDir: string,
  state: BundleBuildState,
  forceAll = false,
): BundleSnapshot {
  const {
    entries,
    sourceHashByFile,
    changedByType,
    processFiles,
    jsEntries,
  } = computeBaseSnapshot(bundle, opts, outDir, state, forceAll)

  const firstRun = state.linkedByEntry.size === 0
  const linkedImpactsByEntry =
    !forceAll && !firstRun
      ? computeLinkedImpacts(changedByType.js, state, processFiles)
      : new Map<string, Set<string>>()

  return {
    entries,
    jsEntries,
    sourceHashByFile,
    changedByType,
    processFiles,
    linkedImpactsByEntry,
  }
}
```

This keeps:

- `BundleSnapshot` shape unchanged,
- `BundleBuildState` unchanged,
- behavior of hashing, change detection, and linked impact computation unchanged,

but makes each concern testable and understandable in isolation, addressing the “one big routine” concern without reverting your structural changes.
</issue_to_address>

### Comment 6
<location path="packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts" line_range="293" />
<code_context>

-    for (const [file, originalSource] of entries) {
-      const type = classifyEntry(file, opts)
+    for (const entry of snapshot.entries) {
+      const { file, output: originalSource, source: originalEntrySource, type } = entry

</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the `snapshot.entries` wrapper with simpler snapshot maps and iterating the original `bundle` to reduce indirection while preserving the new state and dirty-tracking behavior.

You can reduce the new indirection without losing the reuse/state-management improvements from `bundle-state.ts`.

The core pain point is `snapshot.entries`: it wraps `(file, output, source, type)` into an object that is immediately destructured, while the hook already has `bundle` and only needs a couple of derived properties. You can keep all the new capabilities (dirty detection, process sets, jsEntries, linked impacts, hashes) and simplify the hook by shifting `entries` into more focused maps and iterating the original `bundle` again.

### 1. Replace `snapshot.entries` with focused maps

In `bundle-state.ts`, instead of:

```ts
export interface BundleSnapshot {
  // ...
  entries: Array<BundleStateEntry>
  jsEntries: Map<string, OutputEntry>
  processFiles: ProcessFileSets
  changedByType: Record<EntryType, Set<string>>
  linkedImpactsByEntry: Map<string, Set<string>>
  sourceHashByFile: Map<string, string>
}
```

shape it as:

```ts
export interface BundleSnapshot {
  jsEntries: Map<string, OutputEntry>
  processFiles: ProcessFileSets
  changedByType: Record<EntryType, Set<string>>
  linkedImpactsByEntry: Map<string, Set<string>>
  sourceHashByFile: Map<string, string>
  entrySources: Map<string, string> // file -> original entry source
  entryTypes: Map<string, EntryType> // file -> 'html' | 'css' | 'js' | 'other'
}
```

and have `buildBundleSnapshot` fill `entrySources` and `entryTypes` while doing the hashing and classification it already does today.

### 2. Iterate the original bundle in the hook

In the hook, you can then remove the `BundleStateEntry` wrapper usage and go back to a simple `Object.entries(bundle)` loop, but still reuse the precomputed data from the snapshot:

```ts
// before
for (const entry of snapshot.entries) {
  const { file, output: originalSource, source: originalEntrySource, type } = entry
  // ...
}

// after
for (const [file, originalSource] of Object.entries(bundle)) {
  const type = snapshot.entryTypes.get(file) ?? 'other'
  const originalEntrySource =
    snapshot.entrySources.get(file) ??
    (originalSource.type === 'chunk'
      ? originalSource.code
      : originalSource.source.toString())

  if (type === 'html' && originalSource.type === 'asset') {
    metrics.html.total++
    if (!processFiles.html.has(file)) continue

    const rawSource = originalEntrySource
    // ... unchanged cache logic
  }

  if (type === 'css' && originalSource.type === 'asset') {
    metrics.css.total++
    const rawSource = originalEntrySource
    // ... unchanged cache/styleHandler logic
  }

  if (type === 'js') {
    metrics.js.total++
    const shouldTransformJs = processFiles.js.has(file)
    // use originalEntrySource where you now use originalSource.code / .toString()
  }
}
```

This keeps:

- `buildBundleSnapshot` as the single place for classification/hashing.
- `updateBundleBuildState` as the single place for state evolution.
- `jsEntries`, `processFiles`, `changedByType`, `linkedImpactsByEntry`, `sourceHashByFile` unchanged.

But the hook is once again expressed in terms of `bundle` plus a few snapshot lookups, instead of a bespoke `BundleStateEntry` type, which makes the data flow easier to follow and reduces coupling to `bundle-state.ts` internals.
</issue_to_address>

### Comment 7
<location path="packages/weapp-tailwindcss/src/context/compiler-context-cache.ts" line_range="161" />
<code_context>
+  return store
+}
+
+interface ComparableNormalizedValue {
+  normalized: NormalizedOptionsValue
+  sortKey: string
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining the small helper abstractions (`ComparableNormalizedValue`/`createComparableNormalizedValue` and `getRuntimeCacheScopeValue`) directly at their call sites to keep the control flow flatter and easier to follow without changing behavior.

You can trim a couple of abstraction layers without changing behavior or the new caching strategy.

### 1. Inline `ComparableNormalizedValue` / `createComparableNormalizedValue`

The only use of `ComparableNormalizedValue` is to pair `normalized` with a `sortKey`. You can avoid the extra type + helper and do this locally where needed:

```ts
// remove:
// interface ComparableNormalizedValue { ... }
// function createComparableNormalizedValue(...) { ... }

// Set
if (rawValue instanceof Set) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue, element => {
      const normalized = normalizeOptionsValue(element, stack)
      return {
        normalized,
        sortKey: JSON.stringify(normalized),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Set',
      value: normalizedEntries.map(entry => entry.normalized),
    }
  }) as Record<string, NormalizedOptionsValue>
}

// Map
if (rawValue instanceof Map) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue.entries()).map(([key, entryValue]) => {
      const normalizedKey = normalizeOptionsValue(key, stack)
      return {
        key: normalizedKey,
        sortKey: JSON.stringify(normalizedKey),
        value: normalizeOptionsValue(entryValue, stack),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Map',
      value: normalizedEntries.map(entry => [entry.key, entry.value]),
    }
  }) as Record<string, NormalizedOptionsValue>
}
```

This keeps the precomputed sort keys (same perf characteristic) but removes a named type plus a helper that future readers need to chase.

### 2. Inline `getRuntimeCacheScopeValue` at the call site

`getRuntimeCacheScopeValue` is a thin wrapper that makes the call chain deeper. You can keep `getRuntimeCacheScope(opts)` as-is and inline the outer wrapper:

```ts
// remove:
// function getRuntimeCacheScopeValue(opts?: UserDefinedOptions) {
//   return {
//     options: opts ?? {},
//     runtime: getRuntimeCacheScope(opts),
//   }
// }

export function createCompilerContextCacheKey(opts?: UserDefinedOptions): string | undefined {
  try {
    const runtimeCacheScopeKey = createRuntimeCacheScopeKey(opts)
    const keyStore = getCompilerContextKeyCacheStore(opts)
    const cached = keyStore.get(runtimeCacheScopeKey)
    if (cached !== undefined) {
      return cached
    }

    const normalized = normalizeOptionsValue({
      options: opts ?? {},
      runtime: getRuntimeCacheScope(opts),
    })
    const cacheKey = md5Hash(JSON.stringify(normalized))
    keyStore.set(runtimeCacheScopeKey, cacheKey)
    return cacheKey
  }
  catch (error) {
    logger.debug('skip compiler context cache: %O', error)
    return undefined
  }
}
```

This preserves the two-tier cache (`WeakMap` vs `Map`) and runtime scope behavior, but the flow from `createCompilerContextCacheKey` is more straightforward and easier to debug.
</issue_to_address>

### Comment 8
<location path="packages/weapp-tailwindcss/src/core.ts" line_range="63" />
<code_context>
     return resolvedOptions
   }

+  function resolveTransformJsOptions(options?: RuntimeJsTransformOptions): CreateJsHandlerOptions | undefined {
+    if (!options) {
+      const majorVersion = runtimeState.twPatcher.majorVersion
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting small helper functions for default JS and template handler options to avoid duplicated logic and simplify the branching in the transform option resolvers.

You can keep the new behavior (caching + runtime-awareness) but reduce branching and duplication by extracting small helpers.

### 1. Simplify `resolveTransformJsOptions`

Right now `resolveTransformJsOptions` duplicates the “get default by majorVersion” logic and has one extra special-case branch for `runtimeSet`. You can centralize the default logic and always strip `runtimeSet` in one place:

```ts
function getDefaultJsHandlerOptions(): CreateJsHandlerOptions | undefined {
  const majorVersion = runtimeState.twPatcher.majorVersion
  if (typeof majorVersion !== 'number') {
    return undefined
  }

  let cached = defaultJsHandlerOptionsCache.get(majorVersion)
  if (!cached) {
    cached = { tailwindcssMajorVersion: majorVersion }
    defaultJsHandlerOptionsCache.set(majorVersion, cached)
  }
  return cached
}

function resolveTransformJsOptions(
  options?: RuntimeJsTransformOptions,
): CreateJsHandlerOptions | undefined {
  if (!options) {
    return getDefaultJsHandlerOptions()
  }

  const { runtimeSet: _runtimeSet, ...handlerOptions } = options

  if (Object.keys(handlerOptions).length === 0) {
    return getDefaultJsHandlerOptions()
  }

  if (typeof handlerOptions.tailwindcssMajorVersion === 'number') {
    return handlerOptions
  }

  return withRuntimeTailwindMajorVersion(handlerOptions)
}
```

This keeps:

- caching by `majorVersion`
- support for `runtimeSet` on the options object
- “empty options” → default behavior

while reducing the number of decision paths and duplicated code.

### 2. Reduce duplication in `resolveTransformWxmlOptions`

You can keep memoization but wrap it in a tiny “default template options” helper, so `resolveTransformWxmlOptions` only has one branch:

```ts
function getDefaultTemplateHandlerOptions(): ITemplateHandlerOptions {
  if (!cachedDefaultTemplateHandlerOptions || cachedDefaultTemplateRuntimeSet !== runtimeSet) {
    cachedDefaultTemplateRuntimeSet = runtimeSet
    cachedDefaultTemplateHandlerOptions = {
      runtimeSet,
      jsHandler: runtimeAwareTemplateJsHandler,
    }
  }
  return cachedDefaultTemplateHandlerOptions
}

function resolveTransformWxmlOptions(options?: ITemplateHandlerOptions) {
  const base = getDefaultTemplateHandlerOptions()
  return options
    ? defuOverrideArray(options, base)
    : base
}
```

This preserves:

- caching by `runtimeSet`
- `jsHandler` indirection with `runtimeAwareTemplateJsHandler`
- `defuOverrideArray` merging

but makes the control flow for template transforms easier to follow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +148 to +157
function getCompilerContextKeyCacheStore(opts?: UserDefinedOptions) {
if (!opts) {
return compilerContextKeyCacheWithoutOptions
}

let store = compilerContextKeyCacheByOptions.get(opts)
if (!store) {
store = new Map<string, string | undefined>()
compilerContextKeyCacheByOptions.set(opts, store)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): 基于每个 options 的缓存存储如果仅按运行时作用域来做键,可能会忽略后续对 options 的修改。

由于内部的 Map 只使用 runtimeCacheScopeKey 作为 key,任何对被复用的 opts 对象的修改(例如修改 tailwindcssBasedir)都不会影响缓存键。带有已修改选项的调用仍然会复用旧的 { options, runtime } 条目。如果调用方可以修改 opts,可以考虑要么将 opts 视为不可变并在文档中说明,要么在缓存键中加入一个轻量级的 options 指纹,以避免命中过期缓存。

Original comment in English

question (bug_risk): Per-options cache store keyed only by runtime scope may ignore later option mutations.

Because the inner Map is keyed only by runtimeCacheScopeKey, any mutation of a reused opts object (e.g., changing tailwindcssBasedir) won’t affect the cache key. Calls with mutated options will still reuse the old { options, runtime } entry. If callers are allowed to mutate opts, consider either treating opts as immutable and documenting that, or including a lightweight options fingerprint in the key to prevent stale cache hits.

Comment on lines +156 to +164
it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
const token = replaceHandleValue(quasi, {
escapeMap: MappingChars2String,
classNameSet: new Set(['gap-_b20px_B']),
needEscaped: false,
})

expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为 processUpdatedSource 中新的 needEscaped 分支逻辑增加显式覆盖

由于现在在字符串字面量和模板元素之间对 needEscaped 的推导方式不同,可以添加一个聚焦的测试来验证:

  • 构造一个 AST,其中同时包含具有相同 class 值的 StringLiteralTemplateElement
  • 在 options 中不传 needEscaped,调用 processUpdatedSource
  • 断言字符串字面量路径得到的 JsToken.value 对应 needEscaped === true,而模板元素路径得到的 needEscaped === false

这可以直接验证新的分支行为,并防止 needEscaped 处理上的细微回归。

建议实现如下:

  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
    const token = replaceHandleValue(quasi, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      needEscaped: false,
    })

    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
  })

  it('derives needEscaped differently for string literals and template elements', () => {
    const stringLiteral = getLiteralPath('const s = "gap-[20px]"', 'StringLiteral')
    const templateElement = getLiteralPath('const tpl = `gap-[20px]`', 'TemplateElement')

    const stringToken = processUpdatedSource(stringLiteral, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    const templateToken = processUpdatedSource(templateElement, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    expect(stringToken?.value).toBe('gap-_b20px_B')
    expect(stringToken?.needEscaped).toBe(true)

    expect(templateToken?.value).toBe('gap-_b20px_B')
    expect(templateToken?.needEscaped).toBe(false)
  })
  1. 确保在 handlers-branches.test.ts 顶部从与 replaceHandleValue 相同的模块导入 processUpdatedSource(在该仓库中通常类似于 ../../src/js/handlers)。
  2. 如果 JsToken 是一个带类型的对象,并且其他测试中使用了类型断言/类型导入,你也可以导入它的类型以让 TypeScript 更满意(例如 import type { JsToken } from '...'),不过如果依赖类型推断,在测试中这一步是可选的。
Original comment in English

suggestion (testing): Add explicit coverage for the new needEscaped branching in processUpdatedSource

Since needEscaped is now derived differently for string literals vs template elements, it would help to add a focused test that:

  • builds an AST containing both a StringLiteral and a TemplateElement with the same class value,
  • calls processUpdatedSource without passing needEscaped in options,
  • asserts that the string literal path yields JsToken.value with needEscaped === true, and the template element path yields needEscaped === false.

This directly verifies the new branching behavior and guards against subtle regressions in needEscaped handling.

Suggested implementation:

  it('keeps repeated escaped runtime-set hits stable in the same literal', () => {
    const quasi = getLiteralPath('const tpl = `gap-[20px] gap-[20px]`', 'TemplateElement')
    const token = replaceHandleValue(quasi, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      needEscaped: false,
    })

    expect(token?.value).toBe('gap-_b20px_B gap-_b20px_B')
  })

  it('derives needEscaped differently for string literals and template elements', () => {
    const stringLiteral = getLiteralPath('const s = "gap-[20px]"', 'StringLiteral')
    const templateElement = getLiteralPath('const tpl = `gap-[20px]`', 'TemplateElement')

    const stringToken = processUpdatedSource(stringLiteral, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    const templateToken = processUpdatedSource(templateElement, {
      escapeMap: MappingChars2String,
      classNameSet: new Set(['gap-_b20px_B']),
      // intentionally omit needEscaped to exercise default derivation
    })

    expect(stringToken?.value).toBe('gap-_b20px_B')
    expect(stringToken?.needEscaped).toBe(true)

    expect(templateToken?.value).toBe('gap-_b20px_B')
    expect(templateToken?.needEscaped).toBe(false)
  })
  1. Ensure processUpdatedSource is imported at the top of handlers-branches.test.ts from the same module where replaceHandleValue is imported (often something like ../../src/js/handlers or similar in this repo).
  2. If JsToken is a typed object and the tests elsewhere use type assertions/imports, you may also want to import its type to keep TypeScript happy (e.g., import type { JsToken } from '...'), though this is optional in the test if you rely on type inference.

return store
}

interface ComparableNormalizedValue {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 可以考虑将一些小的 helper 抽象(ComparableNormalizedValue/createComparableNormalizedValuegetRuntimeCacheScopeValue)直接内联到各自的调用点,以在不改变行为的情况下让控制流更扁平、更易理解。

你可以在不改变行为和新的缓存策略的前提下,精简掉几层抽象。

1. 内联 ComparableNormalizedValue / createComparableNormalizedValue

ComparableNormalizedValue 唯一的用途就是将 normalizedsortKey 绑在一起。你可以不使用额外的类型和 helper,而是在需要的地方局部完成这件事:

// remove:
// interface ComparableNormalizedValue { ... }
// function createComparableNormalizedValue(...) { ... }

// Set
if (rawValue instanceof Set) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue, element => {
      const normalized = normalizeOptionsValue(element, stack)
      return {
        normalized,
        sortKey: JSON.stringify(normalized),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Set',
      value: normalizedEntries.map(entry => entry.normalized),
    }
  }) as Record<string, NormalizedOptionsValue>
}

// Map
if (rawValue instanceof Map) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue.entries()).map(([key, entryValue]) => {
      const normalizedKey = normalizeOptionsValue(key, stack)
      return {
        key: normalizedKey,
        sortKey: JSON.stringify(normalizedKey),
        value: normalizeOptionsValue(entryValue, stack),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Map',
      value: normalizedEntries.map(entry => [entry.key, entry.value]),
    }
  }) as Record<string, NormalizedOptionsValue>
}

这样可以保留预计算的 sort key(性能特征相同),但移除一个命名类型和一个 helper,避免后续阅读时需要来回跳转。

2. 在调用点内联 getRuntimeCacheScopeValue

getRuntimeCacheScopeValue 只是一个很薄的封装,却让调用链变得更深。你可以保留 getRuntimeCacheScope(opts) 本身,将外层封装直接内联:

// remove:
// function getRuntimeCacheScopeValue(opts?: UserDefinedOptions) {
//   return {
//     options: opts ?? {},
//     runtime: getRuntimeCacheScope(opts),
//   }
// }

export function createCompilerContextCacheKey(opts?: UserDefinedOptions): string | undefined {
  try {
    const runtimeCacheScopeKey = createRuntimeCacheScopeKey(opts)
    const keyStore = getCompilerContextKeyCacheStore(opts)
    const cached = keyStore.get(runtimeCacheScopeKey)
    if (cached !== undefined) {
      return cached
    }

    const normalized = normalizeOptionsValue({
      options: opts ?? {},
      runtime: getRuntimeCacheScope(opts),
    })
    const cacheKey = md5Hash(JSON.stringify(normalized))
    keyStore.set(runtimeCacheScopeKey, cacheKey)
    return cacheKey
  }
  catch (error) {
    logger.debug('skip compiler context cache: %O', error)
    return undefined
  }
}

这样可以保持两级缓存(WeakMap vs Map)和运行时作用域行为不变,但从 createCompilerContextCacheKey 出发的逻辑流会更直观、更易调试。

Original comment in English

issue (complexity): Consider inlining the small helper abstractions (ComparableNormalizedValue/createComparableNormalizedValue and getRuntimeCacheScopeValue) directly at their call sites to keep the control flow flatter and easier to follow without changing behavior.

You can trim a couple of abstraction layers without changing behavior or the new caching strategy.

1. Inline ComparableNormalizedValue / createComparableNormalizedValue

The only use of ComparableNormalizedValue is to pair normalized with a sortKey. You can avoid the extra type + helper and do this locally where needed:

// remove:
// interface ComparableNormalizedValue { ... }
// function createComparableNormalizedValue(...) { ... }

// Set
if (rawValue instanceof Set) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue, element => {
      const normalized = normalizeOptionsValue(element, stack)
      return {
        normalized,
        sortKey: JSON.stringify(normalized),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Set',
      value: normalizedEntries.map(entry => entry.normalized),
    }
  }) as Record<string, NormalizedOptionsValue>
}

// Map
if (rawValue instanceof Map) {
  return withCircularGuard(rawValue, stack, () => {
    const normalizedEntries = Array.from(rawValue.entries()).map(([key, entryValue]) => {
      const normalizedKey = normalizeOptionsValue(key, stack)
      return {
        key: normalizedKey,
        sortKey: JSON.stringify(normalizedKey),
        value: normalizeOptionsValue(entryValue, stack),
      }
    })
    normalizedEntries.sort((a, b) => a.sortKey.localeCompare(b.sortKey))

    return {
      __type: 'Map',
      value: normalizedEntries.map(entry => [entry.key, entry.value]),
    }
  }) as Record<string, NormalizedOptionsValue>
}

This keeps the precomputed sort keys (same perf characteristic) but removes a named type plus a helper that future readers need to chase.

2. Inline getRuntimeCacheScopeValue at the call site

getRuntimeCacheScopeValue is a thin wrapper that makes the call chain deeper. You can keep getRuntimeCacheScope(opts) as-is and inline the outer wrapper:

// remove:
// function getRuntimeCacheScopeValue(opts?: UserDefinedOptions) {
//   return {
//     options: opts ?? {},
//     runtime: getRuntimeCacheScope(opts),
//   }
// }

export function createCompilerContextCacheKey(opts?: UserDefinedOptions): string | undefined {
  try {
    const runtimeCacheScopeKey = createRuntimeCacheScopeKey(opts)
    const keyStore = getCompilerContextKeyCacheStore(opts)
    const cached = keyStore.get(runtimeCacheScopeKey)
    if (cached !== undefined) {
      return cached
    }

    const normalized = normalizeOptionsValue({
      options: opts ?? {},
      runtime: getRuntimeCacheScope(opts),
    })
    const cacheKey = md5Hash(JSON.stringify(normalized))
    keyStore.set(runtimeCacheScopeKey, cacheKey)
    return cacheKey
  }
  catch (error) {
    logger.debug('skip compiler context cache: %O', error)
    return undefined
  }
}

This preserves the two-tier cache (WeakMap vs Map) and runtime scope behavior, but the flow from createCompilerContextCacheKey is more straightforward and easier to debug.

涉及 website/src、.claude/skills、.codex/skills、.github/scripts,
将函数内 regex 提取至模块作用域,修复 prefer-array-at / prefer-spread-syntax /
prefer-timer-args 等规则。
- 修复 packages/weapp-tailwindcss 中 41 个 e18e/prefer-static-regex 错误
- 修复 website/scripts 中 58 个 e18e/prefer-static-regex 错误
- 修复 apps/tailwindcss-weapp/src/env.d.ts 中过时的 eslint-disable 注释
- eslint.config.js 新增 ignore 规则:markdown、test、benchmark、skills、d.ts 等
- website/eslint.config.mjs 将 better-tailwindcss 规则降级为 warn
- packages-runtime/ui/vite.config.ts: 提取 /\.css$/ 正则到模块作用域
- vitest.config.ts: 提取 extractBaseDirFromGlob 中的正则到模块作用域
- website/config/blog.ts: 提取 require 调用并添加 eslint-disable 注释
- packages-runtime/ui/eslint.config.js: 忽略 test/scripts 目录,降级 better-tailwindcss 规则
@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 88.59447% with 198 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.75%. Comparing base (34d9351) to head (1f0742f).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...src/bundlers/vite/incremental-runtime-class-set.ts 85.08% 31 Missing and 3 partials ⚠️
packages/postcss/src/options-resolver.ts 73.19% 21 Missing and 5 partials ⚠️
packages/weapp-tailwindcss/src/core.ts 73.61% 15 Missing and 4 partials ⚠️
packages/weapp-tailwindcss/src/js/handlers.ts 77.38% 13 Missing and 6 partials ⚠️
...cripts/watch-hmr-regression/cases/demo/extended.ts 8.33% 11 Missing ⚠️
...p-tailwindcss/scripts/watch-hmr-regression/text.ts 77.55% 9 Missing and 2 partials ⚠️
packages/postcss/src/processor-cache.ts 67.74% 10 Missing ⚠️
...s/src/bundlers/vite/runtime-affecting-signature.ts 84.00% 7 Missing and 1 partial ⚠️
...ss/scripts/watch-hmr-regression/cases/demo/base.ts 0.00% 6 Missing ⚠️
...ailwindcss/scripts/watch-hmr-regression/session.ts 45.45% 5 Missing and 1 partial ⚠️
... and 20 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #819      +/-   ##
==========================================
- Coverage   86.34%   82.75%   -3.59%     
==========================================
  Files         245      254       +9     
  Lines        8693    10394    +1701     
  Branches     2576     3027     +451     
==========================================
+ Hits         7506     8602    +1096     
- Misses       1003     1522     +519     
- Partials      184      270      +86     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant