Conversation
🦋 Changeset detectedLatest commit: 1f0742f The changes in this PR will be included in the next version bump. This PR includes no changesetsWhen 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 |
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
更新后的 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
更新后的核心上下文转换辅助方法与 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
文件级变更
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideRefactors 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 cachingsequenceDiagram
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
Updated class diagram for Vite bundle-state and build snapshotclassDiagram
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
Updated class diagram for core context transform helpers and handler options cachingclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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 = {
'&': '&',
'<': '<',
}
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>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的 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 = {
'&': '&',
'<': '<',
}
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
packages/weapp-tailwindcss/test/core.transform-wxss-options.unit.test.ts
Show resolved
Hide resolved
| 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') |
There was a problem hiding this comment.
suggestion (testing): 为 processUpdatedSource 中新的 needEscaped 分支逻辑增加显式覆盖
由于现在在字符串字面量和模板元素之间对 needEscaped 的推导方式不同,可以添加一个聚焦的测试来验证:
- 构造一个 AST,其中同时包含具有相同 class 值的
StringLiteral和TemplateElement; - 在 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)
})- 确保在
handlers-branches.test.ts顶部从与replaceHandleValue相同的模块导入processUpdatedSource(在该仓库中通常类似于../../src/js/handlers)。 - 如果
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
StringLiteraland aTemplateElementwith the same class value, - calls
processUpdatedSourcewithout passingneedEscapedin options, - asserts that the string literal path yields
JsToken.valuewithneedEscaped === true, and the template element path yieldsneedEscaped === 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)
})- Ensure
processUpdatedSourceis imported at the top ofhandlers-branches.test.tsfrom the same module wherereplaceHandleValueis imported (often something like../../src/js/handlersor similar in this repo). - If
JsTokenis 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 { |
There was a problem hiding this comment.
issue (complexity): 可以考虑将一些小的 helper 抽象(ComparableNormalizedValue/createComparableNormalizedValue 和 getRuntimeCacheScopeValue)直接内联到各自的调用点,以在不改变行为的情况下让控制流更扁平、更易理解。
你可以在不改变行为和新的缓存策略的前提下,精简掉几层抽象。
1. 内联 ComparableNormalizedValue / createComparableNormalizedValue
ComparableNormalizedValue 唯一的用途就是将 normalized 与 sortKey 绑在一起。你可以不使用额外的类型和 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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
- native-mina: 在 Windows 上为 spawn 添加 shell: true 选项 - mpx-tailwindcss-v4: 禁用 webpack 持久化缓存避免序列化器重复注册
monkey-patch ObjectMiddleware.register 忽略重复注册错误, 该问题由 pnpm + webpack5 环境下模块多路径加载触发
|



变更说明
验证
Summary by Sourcery
改进热路径缓存以及在各打包器和核心转换中的默认处理器选项复用,以减少增量构建中的重复计算。
Enhancements:
Tests:
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:
Tests: