Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clean-empty-media-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@weapp-tailwindcss/postcss": patch
"weapp-tailwindcss": patch
---

修复小程序样式在非主样式块、缓存产物与嵌套条件规则中残留空 `@media`、`@supports` 等块级 at-rule,避免生成的 WXSS 因空媒体查询触发编译错误。
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export interface FinalizeMiniProgramCssOptions {
cssPreflight?: CssPreflightOptions | undefined
cssSelectorReplacement?: CssSelectorReplacement | undefined
isTailwindcssV4?: boolean | undefined
/**
* 是否递归移除子规则被清理而变空的父级条件规则。
*
* 增量生成的 CSS 可能只包含条件规则中的新片段,父级容器由已缓存产物提供,
* 此时应保留占位容器,避免后续追加 CSS 时丢失层级。
*/
removeEmptyAtRuleAncestors?: boolean | undefined
/**
* 是否为 Tailwind CSS v4 渐变工具类生成小程序字面量兜底。
*/
Expand Down
11 changes: 10 additions & 1 deletion packages/postcss/src/compat/mini-program-css/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ function finalizeMiniProgramCssRoot(root: postcss.Root, options: FinalizeMiniPro
const themeRule = collectThemeVariableRule(root, options)
const hoistedRules = themeRule ? [...preflightRules, themeRule] : preflightRules
insertHoistedRules(root, mergeEquivalentHoistedRules(hoistedRules), hoistAnchor)
removeEmptyAtRules(root)
if (options.removeEmptyAtRuleAncestors !== false) {
removeEmptyAtRules(root)
}
else {
root.walkAtRules((atRule) => {
if (atRule.nodes?.length === 0) {
atRule.remove()
}
})
}
}

export function hoistTailwindPreflightBase(css: string) {
Expand Down
1 change: 1 addition & 0 deletions packages/postcss/src/compat/mini-program-css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export {
} from './prune-generated'
export {
hasMiniProgramCssSpecificityPlaceholders,
removeEmptyAtRules,
stripMiniProgramCssSpecificityPlaceholders,
} from './root-cleanups'
24 changes: 23 additions & 1 deletion packages/postcss/src/compat/mini-program-css/root-cleanups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,33 @@ function isEffectivelyEmptyContainer(container: postcss.Container) {
}

export function removeEmptyAtRules(root: postcss.Root) {
let removed = 0
const visit = (container: postcss.Container) => {
for (const node of [...(container.nodes ?? [])]) {
if (!('nodes' in node) || node.nodes === undefined) {
continue
}
visit(node)
if (node.type === 'atrule' && node.parent && isEffectivelyEmptyContainer(node)) {
node.remove()
removed++
}
}
}

visit(root)
return removed
}

export function removeEmptyBlockAtRules(root: postcss.Root) {
let removed = 0
root.walkAtRules((atRule) => {
if (isEffectivelyEmptyContainer(atRule)) {
if (atRule.nodes?.length === 0) {
atRule.remove()
removed++
}
})
return removed
}

function removeEmptyAtRuleAncestors(parent: postcss.Container | undefined) {
Expand Down
14 changes: 14 additions & 0 deletions packages/postcss/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defuOverrideArray } from '@weapp-tailwindcss/shared'
import { LRUCache } from 'lru-cache'
import postcss from 'postcss'
import { protectDynamicColorMixAlpha } from './compat/color-mix'
import { removeEmptyBlockAtRules } from './compat/mini-program-css/root-cleanups'
import { probeFeatures, signalToCacheKey } from './content-probe'
import { getDefaultOptions } from './defaults'
import { fingerprintOptions } from './fingerprint'
Expand Down Expand Up @@ -128,6 +129,19 @@ export function createStyleHandler(options?: Partial<IStyleHandlerOptions>): Sty
).async().then((result) => {
const styleBranch = resolvePostcssFrameworkProfile(resolvedOptions)
let finalResult = styleBranch.postprocess(result, resolvedOptions)
if (resolvedOptions.isMainChunk !== false && finalResult.root) {
let removed = 0
let removedTotal = 0
do {
removed = removeEmptyBlockAtRules(finalResult.root)
removedTotal += removed
} while (removed > 0)
if (removedTotal > 0) {
const nextResult = finalResult.root.toResult(finalResult.opts)
nextResult.messages.push(...finalResult.messages)
finalResult = nextResult
}
}
if (protectedColorMix) {
const restoredCss = protectedColorMix.restore(finalResult.css)
if (restoredCss !== finalResult.css) {
Expand Down
1 change: 1 addition & 0 deletions packages/postcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
hoistTailwindPreflightBase,
normalizeMiniProgramGeneratedCssForPostcss,
pruneMiniProgramGeneratedCss,
removeEmptyAtRules,
removeUnsupportedAtSupports,
removeUnsupportedCascadeLayers,
removeUnsupportedMiniProgramAtRules,
Expand Down
17 changes: 17 additions & 0 deletions packages/postcss/test/mini-program-css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,23 @@ describe('mini-program css cleanup', () => {
expect(css).not.toContain('margin')
})

it('keeps incremental at-rule ancestors when recursive cleanup is disabled', () => {
const css = finalizeMiniProgramCss('@media screen{/* incremental placeholder */}', {
cssPreflight: false,
removeEmptyAtRuleAncestors: false,
})

expect(css).toBe('@media screen{/* incremental placeholder */}')
})

it('recursively removes empty at-rule ancestors for complete css output', () => {
const css = finalizeMiniProgramCss('@media screen{@supports (display:grid){}}', {
cssPreflight: false,
})

expect(css).toBe('')
})

it('prunes browser-only generated css while preserving useful mini-program selectors', () => {
const css = pruneMiniProgramGeneratedCss([
'/* #ifdef MP-WEIXIN */',
Expand Down
13 changes: 13 additions & 0 deletions packages/postcss/test/mini-program-generated-css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ describe('mini-program generated css cleanup', () => {
expect(css).not.toContain('box-sizing:border-box')
})

it('removes empty conditional at-rules from generated mini-program css', () => {
const css = finalizeMiniProgramCss([
'@media (prefers-color-scheme: light) {}',
'@media (prefers-color-scheme: dark) { /* removed declarations */ }',
'@media screen { @supports (display: grid) {} }',
'.keep{color:red}',
].join('\n'), { isTailwindcssV4: true })

expect(css).not.toContain('@media')
expect(css).not.toContain('@supports')
expect(css).toContain('.keep{color:red}')
})

it('preserves user page custom properties that use Tailwind v4 theme namespaces', async () => {
const styleHandler = createStyleHandler({
majorVersion: 4,
Expand Down
31 changes: 31 additions & 0 deletions packages/postcss/test/post.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import postcss from 'postcss'
import { createStyleHandler } from '@/handler'
import { postcssWeappTailwindcssPostPlugin } from '@/plugins/post'

describe('postcss post plugin', () => {
Expand Down Expand Up @@ -26,4 +27,34 @@ describe('postcss post plugin', () => {
]).process(rawCode)
expect(css).toMatchSnapshot()
})

it('preserves standalone conditional placeholders for incremental css assembly', async () => {
const input = '@media (min-width: 64rem) { /* incremental placeholder */ }'
const result = await postcss([
postcssWeappTailwindcssPostPlugin({
isMainChunk: true,
}),
]).process(input, { from: undefined })

expect(result.css).toBe(input)
})

it('removes nested empty blocks after the postcss lifecycle completes', async () => {
const styleHandler = createStyleHandler({
isMainChunk: true,
})
const { css } = await styleHandler('@media (min-width: 64rem) { @supports (display: grid) {} }')

expect(css).toBe('')
})

it('keeps comment-only incremental placeholders after postprocessing', async () => {
const input = '@media (min-width: 64rem) { /* incremental placeholder */ }'
const styleHandler = createStyleHandler({
isMainChunk: true,
})
const { css } = await styleHandler(input)

expect(css).toBe(input)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export function finalizeMiniProgramGeneratorCss(
target: string,
_majorVersion: number | undefined,
cssPreflight: InternalUserDefinedOptions['cssPreflight'],
options: { injectPreflight?: boolean, preservePreflight?: boolean, styleOptions?: Partial<IStyleHandlerOptions> | undefined } = {},
options: {
injectPreflight?: boolean | undefined
preservePreflight?: boolean | undefined
removeEmptyAtRuleAncestors?: boolean | undefined
styleOptions?: Partial<IStyleHandlerOptions> | undefined
} = {},
) {
if (!isMiniProgramGeneratorTarget(target)) {
return css
Expand All @@ -29,6 +34,7 @@ export function finalizeMiniProgramGeneratorCss(
cssSelectorReplacement: options.styleOptions?.cssOptions?.cssSelectorReplacement
?? options.styleOptions?.cssSelectorReplacement,
isTailwindcssV4: true,
removeEmptyAtRuleAncestors: options.removeEmptyAtRuleAncestors,
tailwindcssV4GradientFallback: options.styleOptions?.cssOptions?.tailwindcssV4GradientFallback
?? options.styleOptions?.tailwindcssV4GradientFallback,
})
Expand All @@ -48,6 +54,7 @@ export function finalizeMiniProgramGeneratorCss(
cssSelectorReplacement: options.styleOptions?.cssOptions?.cssSelectorReplacement
?? options.styleOptions?.cssSelectorReplacement,
isTailwindcssV4: true,
removeEmptyAtRuleAncestors: options.removeEmptyAtRuleAncestors,
tailwindcssV4GradientFallback: options.styleOptions?.cssOptions?.tailwindcssV4GradientFallback
?? options.styleOptions?.tailwindcssV4GradientFallback,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface GeneratorPipelineExecutionContext {
options?: {
injectPreflight?: boolean | undefined
preservePreflight?: boolean | undefined
removeEmptyAtRuleAncestors?: boolean | undefined
styleOptions?: Partial<IStyleHandlerOptions> | undefined
},
) => string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function finalizeOrderedGeneratorCss(
const css = incrementalCss.trim().length > 0
? finalizeIncrementalGeneratorCss(options.previousCss, incrementalCss, generated.target, majorVersion, opts.cssPreflight, {
injectPreflight: false,
removeEmptyAtRuleAncestors: false,
styleOptions: generatorStyleOptions,
}, generatorOptions.webCompat)
: options.previousCss
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
stripMiniProgramCssSpecificityPlaceholders,
} from '@/bundlers/shared/css-cleanup'
import { AssetEmissionPlan } from '@/compiler'
import { removeEmptyCssAtRules } from '../processed-css-assets/cleanup'
import { applyViteAssetEmissionPlan } from './asset-emission-plan'

function readAssetSource(output: OutputAsset) {
Expand All @@ -30,6 +31,7 @@ export async function finalizeMiniProgramCssAssets(
onUpdate: GenerateBundleContext['opts']['onUpdate']
recordCssAssetResult: GenerateBundleContext['recordCssAssetResult']
styleHandler: GenerateBundleContext['opts']['styleHandler']
useIncrementalMode?: boolean | undefined
debug?: GenerateBundleContext['debug']
},
) {
Expand All @@ -54,7 +56,10 @@ export async function finalizeMiniProgramCssAssets(
continue
}
if (options.lastCssResultByFile?.has(file)) {
const outputCss = stripMiniProgramCssSpecificityPlaceholders(rawSource)
const structurallyCleanSource = options.useIncrementalMode
? rawSource
: removeEmptyCssAtRules(rawSource)
const outputCss = stripMiniProgramCssSpecificityPlaceholders(structurallyCleanSource)
if (outputCss !== rawSource) {
plan.write(file, outputCss)
writeTargets.set(file, output)
Expand All @@ -66,6 +71,17 @@ export async function finalizeMiniProgramCssAssets(
continue
}
if (!shouldFinalizeMiniProgramCssAsset(rawSource)) {
const structurallyCleanSource = options.useIncrementalMode
? rawSource
: removeEmptyCssAtRules(rawSource)
if (structurallyCleanSource !== rawSource) {
plan.write(file, structurallyCleanSource)
writeTargets.set(file, output)
options.recordCssAssetResult?.(file, structurallyCleanSource)
options.onUpdate(file, rawSource, structurallyCleanSource)
options.debug?.('remove empty mini-program css at-rules: %s bytes=%d', file, structurallyCleanSource.length)
updated++
}
continue
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export async function finalizeGenerateBundle(options: FinalizeGenerateBundleOpti
onUpdate,
recordCssAssetResult,
styleHandler,
useIncrementalMode,
})
recordTimingDetail('finalize.cssAssets', finalCssAssetsStartedAt)
const webCompatStartedAt = performance.now()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { OutputAsset, OutputBundle } from 'rollup'
import type { CollectViteProcessedCssAssetOptions } from './markers-imports'
import type { InternalUserDefinedOptions } from '@/types'
import { isMiniProgramLocalCssImportRequest, parseTailwindCssDirectiveRequest, postcss } from '@weapp-tailwindcss/postcss'
import { isMiniProgramLocalCssImportRequest, parseTailwindCssDirectiveRequest, postcss, removeEmptyAtRules } from '@weapp-tailwindcss/postcss'
import path from 'pathe'
import { normalizeOutputPathKey } from '../../shared/module-graph'
import { hasEmptyAtRuleBlockCandidate } from './empty-at-rule'
import { appendCss, collectImportedStyleFiles, createCssAssetPipelineContext, getAssetFile, isStyleImportRequest, readAssetSource } from './markers-imports'
import { isMiniProgramStyleOutputFile, isRootStyleOutputFile } from './style-files'

Expand Down Expand Up @@ -71,7 +72,7 @@ export function restoreCssImportAtRules(source: string, filtered: string, file?:
}

export function removeCommentOnlyAtRules(css: string) {
if (!css.includes('@')) {
if (!hasEmptyAtRuleBlockCandidate(css)) {
return css
}
try {
Expand All @@ -95,6 +96,25 @@ export function removeCommentOnlyAtRules(css: string) {
}
}

export function removeEmptyCssAtRules(css: string) {
if (!hasEmptyAtRuleBlockCandidate(css)) {
return css
}
try {
const root = postcss.parse(css)
let removed = 0
let passRemoved = 0
do {
passRemoved = removeEmptyAtRules(root)
removed += passRemoved
} while (passRemoved > 0)
return removed > 0 ? root.toString() : css
}
catch {
return css
}
}

export function collectImportedBundleCssSources(bundle: OutputBundle, importedStyleFiles: Set<string>) {
if (importedStyleFiles.size === 0) {
return []
Expand Down
Loading
Loading