From af1451a9adc9edf9e0fe55aaff7c5c2650fa4152 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 20:13:03 +0800 Subject: [PATCH 1/7] refactor: move postcss entrypoints into postcss package --- .changeset/move-postcss-entrypoints.md | 6 + packages/postcss/package.json | 13 +- packages/postcss/src/css-macro/auto.ts | 314 ++++++++++++++++ packages/postcss/src/css-macro/constants.ts | 106 ++++++ packages/postcss/src/css-macro/postcss.ts | 158 +++++++++ .../src/generator-plugin}/config-directive.ts | 0 .../src/generator-plugin}/context.ts | 5 +- .../src/generator-plugin/directives.ts | 46 +++ .../postcss/src/generator-plugin/index.ts | 176 +++++++++ .../src/generator-plugin/package-version.ts | 85 +++++ .../src/generator-plugin}/source-files.ts | 13 +- .../src/generator-plugin}/tailwind-version.ts | 4 +- .../postcss/src/generator-plugin/types.ts | 114 ++++++ packages/postcss/src/index.ts | 25 ++ packages/postcss/src/source-scan.ts | 335 ++++++++++++++++++ .../postcss/src/source-scan/inline-source.ts | 193 ++++++++++ .../postcss/test/generator-plugin.test.ts | 95 +++++ packages/postcss/tsdown.config.mts | 2 +- packages/postcss/vitest.config.ts | 12 + .../weapp-tailwindcss/src/css-macro/auto.ts | 314 +--------------- .../src/css-macro/postcss.ts | 163 +-------- packages/weapp-tailwindcss/src/postcss.ts | 207 +---------- .../test/css-macro/index.test.ts | 45 ++- packages/weapp-tailwindcss/tsconfig.json | 3 + packages/weapp-tailwindcss/vitest.config.ts | 20 ++ pnpm-lock.yaml | 9 + 26 files changed, 1784 insertions(+), 679 deletions(-) create mode 100644 .changeset/move-postcss-entrypoints.md create mode 100644 packages/postcss/src/css-macro/auto.ts create mode 100644 packages/postcss/src/css-macro/constants.ts create mode 100644 packages/postcss/src/css-macro/postcss.ts rename packages/{weapp-tailwindcss/src/postcss => postcss/src/generator-plugin}/config-directive.ts (100%) rename packages/{weapp-tailwindcss/src/postcss => postcss/src/generator-plugin}/context.ts (90%) create mode 100644 packages/postcss/src/generator-plugin/directives.ts create mode 100644 packages/postcss/src/generator-plugin/index.ts create mode 100644 packages/postcss/src/generator-plugin/package-version.ts rename packages/{weapp-tailwindcss/src/postcss => postcss/src/generator-plugin}/source-files.ts (92%) rename packages/{weapp-tailwindcss/src/postcss => postcss/src/generator-plugin}/tailwind-version.ts (90%) create mode 100644 packages/postcss/src/generator-plugin/types.ts create mode 100644 packages/postcss/src/source-scan.ts create mode 100644 packages/postcss/src/source-scan/inline-source.ts create mode 100644 packages/postcss/test/generator-plugin.test.ts diff --git a/.changeset/move-postcss-entrypoints.md b/.changeset/move-postcss-entrypoints.md new file mode 100644 index 000000000..d5dd5423a --- /dev/null +++ b/.changeset/move-postcss-entrypoints.md @@ -0,0 +1,6 @@ +--- +"weapp-tailwindcss": patch +"@weapp-tailwindcss/postcss": patch +--- + +将 `weapp-tailwindcss` 中生成型 PostCSS 插件、PostCSS 辅助扫描逻辑和 `css-macro/postcss` 转换入口迁入 `@weapp-tailwindcss/postcss`,主包保留兼容转发入口,方便后续统一维护 PostCSS 能力边界。 diff --git a/packages/postcss/package.json b/packages/postcss/package.json index 24aee491d..36bece0e4 100644 --- a/packages/postcss/package.json +++ b/packages/postcss/package.json @@ -30,6 +30,11 @@ "import": "./dist/html-transform.mjs", "require": "./dist/html-transform.js" }, + "./css-macro/postcss": { + "types": "./dist/css-macro/postcss.d.ts", + "import": "./dist/css-macro/postcss.mjs", + "require": "./dist/css-macro/postcss.js" + }, "./package.json": "./package.json" }, "main": "./dist/index.js", @@ -40,6 +45,9 @@ "html-transform": [ "./dist/html-transform.d.ts" ], + "css-macro/postcss": [ + "./dist/css-macro/postcss.d.ts" + ], "types": [ "./dist/types.d.ts" ], @@ -72,12 +80,15 @@ "@weapp-tailwindcss/shared": "workspace:*", "autoprefixer": "catalog:autoprefixer10", "lru-cache": "11.5.1", + "micromatch": "^4.0.8", "postcss": "catalog:postcss85tilde", "postcss-pxtrans": "^1.0.4", "postcss-rem-to-responsive-pixel": "catalog:postcssRem", "postcss-rule-unit-converter": "^0.2.2", "postcss-selector-parser": "~7.1.2", - "postcss-value-parser": "^4.2.0" + "postcss-value-parser": "^4.2.0", + "tailwindcss-config": "workspace:*", + "tailwindcss-patch": "catalog:tailwindcssPatch" }, "devDependencies": { "@csstools/css-color-parser": "^4.1.1", diff --git a/packages/postcss/src/css-macro/auto.ts b/packages/postcss/src/css-macro/auto.ts new file mode 100644 index 000000000..23b3e1fef --- /dev/null +++ b/packages/postcss/src/css-macro/auto.ts @@ -0,0 +1,314 @@ +import type { ResultPlugin } from 'postcss-load-config' +import type { IStyleHandlerOptions, LoadedPostcssOptions } from '../types' +import path from 'node:path' +import process from 'node:process' +import postcss from 'postcss' +import cssMacroPostcssPlugin, { CSS_MACRO_POSTCSS_PLUGIN_NAME } from './postcss' + +export const CSS_MACRO_STYLE_OPTIONS_MARKER = '__weappTailwindcssCssMacroEnabled' + +type CssMacroStyleOptions = Partial & { + [CSS_MACRO_STYLE_OPTIONS_MARKER]?: true +} + +type ConditionalValue = boolean | undefined + +const PLATFORM_ENV_KEYS = [ + 'WEAPP_TW_TARGET', + 'WEAPP_TAILWINDCSS_TARGET', + 'UNI_PLATFORM', + 'UNI_UTS_PLATFORM', + 'TARO_ENV', + 'MPX_CLI_MODE', + 'MPX_CURRENT_TARGET_MODE', +] as const + +const CONDITIONAL_END_RE = /^\s*#endif\s*$/ + +function readEnvValue(key: string): string | undefined { + return typeof process === 'undefined' ? undefined : process.env[key] +} + +function normalizePlatformToken(value: string | undefined): string | undefined { + const normalized = value?.trim().replaceAll('_', '-').toUpperCase() + return normalized || undefined +} + +function resolveCssMacroPlatform(options: Pick | undefined): string | undefined { + const explicit = normalizePlatformToken(options?.platform) + if (explicit) { + return explicit + } + + for (const key of PLATFORM_ENV_KEYS) { + const value = normalizePlatformToken(readEnvValue(key)) + if (value) { + return value + } + } + + return undefined +} + +function createPlatformTokenSet(platform: string | undefined) { + const normalized = normalizePlatformToken(platform) + const tokens = new Set() + if (!normalized) { + return tokens + } + + tokens.add(normalized) + if (normalized.startsWith('MP-')) { + tokens.add('MP') + } + if (normalized === 'WEAPP' || normalized === 'WEIXIN' || normalized === 'WX') { + tokens.add('MP') + tokens.add('MP-WEIXIN') + } + if (normalized === 'MP-WEIXIN') { + tokens.add('WEAPP') + tokens.add('WEIXIN') + tokens.add('WX') + } + if (normalized === 'H5') { + tokens.add('WEB') + } + if (normalized === 'WEB') { + tokens.add('H5') + } + if (normalized === 'APP') { + tokens.add('APP-PLUS') + } + if (normalized.startsWith('APP-')) { + tokens.add('APP') + } + if (normalized.startsWith('QUICKAPP-WEBVIEW')) { + tokens.add('QUICKAPP-WEBVIEW') + } + return tokens +} + +function combineAnd(values: ConditionalValue[]): ConditionalValue { + if (values.includes(false)) { + return false + } + return values.every(value => value === true) ? true : undefined +} + +function combineOr(values: ConditionalValue[]): ConditionalValue { + if (values.includes(true)) { + return true + } + return values.every(value => value === false) ? false : undefined +} + +function evaluatePlatformExpression(expression: string, platformTokens: ReadonlySet): ConditionalValue { + const orParts = expression.split(/\s*\|\|\s*/) + const orValues = orParts.map((orPart) => { + const andParts = orPart.split(/\s*&&\s*/) + return combineAnd(andParts.map((part) => { + const token = normalizePlatformToken(part) + if (!token || /[<>=!()]/.test(token)) { + return undefined + } + return platformTokens.has(token) + })) + }) + return combineOr(orValues) +} + +function negateConditionalValue(value: ConditionalValue): ConditionalValue { + return value === undefined ? undefined : !value +} + +function getActiveConditionalValue(stack: ConditionalValue[]): ConditionalValue { + if (stack.includes(false)) { + return false + } + return stack.includes(undefined) ? undefined : true +} + +function parseConditionalStart(text: string) { + const normalized = text.trim() + if (!normalized.startsWith('#')) { + return undefined + } + + const body = normalized.slice(1).trimStart() + const directives = ['ifndef', 'ifdef'] as const + for (const directive of directives) { + if (!body.startsWith(directive)) { + continue + } + const expression = body.slice(directive.length).trim() + if (expression.length === 0) { + return undefined + } + return { + directive, + expression, + } + } + + return undefined +} + +export function compileCssMacroConditionalComments( + css: string, + options?: Pick, +): string { + const platform = resolveCssMacroPlatform(options) + const platformTokens = createPlatformTokenSet(platform) + if (platformTokens.size === 0 || !css.includes('#if')) { + return css + } + + try { + const root = postcss.parse(css) + const transformContainer = (container: postcss.Container) => { + const stack: ConditionalValue[] = [] + for (const node of [...container.nodes ?? []]) { + if (node.type === 'comment') { + const start = parseConditionalStart(node.text) + if (start) { + const value = start.directive === 'ifndef' + ? negateConditionalValue(evaluatePlatformExpression(start.expression, platformTokens)) + : evaluatePlatformExpression(start.expression, platformTokens) + const parentActive = getActiveConditionalValue(stack) + stack.push(value) + if (parentActive !== undefined && value !== undefined) { + node.remove() + } + continue + } + + if (CONDITIONAL_END_RE.test(node.text)) { + const value = stack.pop() + const parentActive = getActiveConditionalValue(stack) + if (parentActive !== undefined && value !== undefined) { + node.remove() + } + continue + } + } + + if (getActiveConditionalValue(stack) === false) { + node.remove() + continue + } + + if ('nodes' in node && node.nodes) { + transformContainer(node) + } + } + } + transformContainer(root) + return root.toString() + } + catch { + return css + } +} + +function parseCssPluginRequest(params: string) { + const value = params.trim() + const quoted = /^(['"])(.*?)\1/.exec(value) + if (quoted) { + return quoted[2] + } + + const url = /^url\(\s*(?:(['"])(.*?)\1|([^'")\s]+))\s*\)/.exec(value) + return url?.[2] ?? url?.[3] +} + +function isCssMacroPluginRequest(request: string | undefined) { + if (request === 'weapp-tailwindcss/css-macro') { + return true + } + if (!request?.includes('css-macro')) { + return false + } + return path.basename(request).startsWith('css-macro') +} + +export function hasCssMacroTailwindV4Directive(css: string | undefined): boolean { + if (!css?.includes('css-macro')) { + return false + } + + try { + let found = false + postcss.parse(css).walkAtRules('plugin', (rule) => { + if (isCssMacroPluginRequest(parseCssPluginRequest(rule.params))) { + found = true + } + }) + return found + } + catch { + return /@plugin\s+(?:url\(\s*)?["']weapp-tailwindcss\/css-macro["']/.test(css) + } +} + +function isCssMacroPostcssPlugin(plugin: unknown): boolean { + if (plugin === cssMacroPostcssPlugin) { + return true + } + return Boolean( + plugin + && (typeof plugin === 'function' || typeof plugin === 'object') + && (plugin as { postcssPlugin?: unknown }).postcssPlugin === CSS_MACRO_POSTCSS_PLUGIN_NAME, + ) +} + +function withCssMacroPostcssPlugins(plugins: unknown): LoadedPostcssOptions['plugins'] { + const macroPlugin = cssMacroPostcssPlugin() + + if (!plugins) { + return [macroPlugin] + } + + if (Array.isArray(plugins)) { + return plugins.some(isCssMacroPostcssPlugin) + ? plugins + : [...plugins, macroPlugin] + } + + if (typeof plugins === 'object') { + const values = Object.values(plugins as Record).filter(Boolean) as ResultPlugin[] + if (values.some(isCssMacroPostcssPlugin)) { + return values + } + return [...values, macroPlugin] + } + + return [macroPlugin] +} + +export function withCssMacroStyleOptions( + options: Partial | undefined, +): Partial { + const postcssOptions = options?.postcssOptions + return { + ...options, + [CSS_MACRO_STYLE_OPTIONS_MARKER]: true, + postcssOptions: { + ...postcssOptions, + plugins: withCssMacroPostcssPlugins(postcssOptions?.plugins), + }, + } as CssMacroStyleOptions +} + +export function hasCssMacroStyleOptions(options: Partial | undefined): boolean { + return Boolean((options as CssMacroStyleOptions | undefined)?.[CSS_MACRO_STYLE_OPTIONS_MARKER]) +} + +export async function transformCssMacroCss( + css: string, + options?: Pick, +): Promise { + const result = (await postcss([cssMacroPostcssPlugin()]).process(css, { + from: undefined, + })).css + return compileCssMacroConditionalComments(result, options) +} diff --git a/packages/postcss/src/css-macro/constants.ts b/packages/postcss/src/css-macro/constants.ts new file mode 100644 index 000000000..6c494efb4 --- /dev/null +++ b/packages/postcss/src/css-macro/constants.ts @@ -0,0 +1,106 @@ +// 参考:https://uniapp.dcloud.net.cn/tutorial/platform.html#%E6%A0%B7%E5%BC%8F%E7%9A%84%E6%9D%A1%E4%BB%B6%E7%BC%96%E8%AF%91 +// %PLATFORM% 条件编译占位符 +export const uniAppPlatform = [ + // 一般不会用到的枚举项 + 'VUE3', + 'UNI-APP-X', + 'uniVersion', + 'APP', + 'APP-PLUS', + 'APP-PLUS-NVUE', + 'APP-NVUE', + 'APP-ANDROID', + 'APP-IOS', + 'H5', + 'WEB', + 'MP-WEIXIN', + 'MP-ALIPAY', + 'MP-BAIDU', + 'MP-TOUTIAO', + 'MP-LARK', + 'MP-QQ', + 'MP-KUAISHOU', + 'MP-JD', + 'MP-360', + 'MP', + 'QUICKAPP-WEBVIEW', + 'QUICKAPP-WEBVIEW-UNION', + 'QUICKAPP-WEBVIEW-HUAWEI', +] + +// 预留:staticVariants 配置占位 +export const queryKey = 'weapp-tw-platform' +export const ifdefAtRule = 'weapp-tw-ifdef' +export const ifndefAtRule = 'weapp-tw-ifndef' + +function quoteAtRuleParam(value: string) { + return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` +} + +export function createConditionalAtRule(value: string) { + return `@${ifdefAtRule} ${quoteAtRuleParam(value)}{&}` +} + +export function createNegativeConditionalAtRule(value: string) { + return `@${ifndefAtRule} ${quoteAtRuleParam(value)}{&}` +} + +const UNESCAPED_UNDERSCORE_RE = /(? 3.9 +// H5 || MP-WEIXIN +// not screen and (weapp-tw-platform:MP-WEIXIN) +const QUERY_KEY_REGEX = new RegExp(`\\(\\s*${queryKey}\\s*:\\s*"([^)]*)"\\)`, 'g') + +export function matchCustomPropertyFromValue(str: string, cb: (arr: RegExpExecArray, index: number) => void) { + let index = 0 + + // 重置 lastIndex 以确保每次调用从头匹配 + QUERY_KEY_REGEX.lastIndex = 0 + let arr = QUERY_KEY_REGEX.exec(str) + while (arr !== null) { + cb(arr, index) + index++ + arr = QUERY_KEY_REGEX.exec(str) + } +} + +export function parseConditionalAtRuleParam(params: string) { + const value = params.trim() + const quoted = /^(['"])((?:\\.|(?!\1).)*)\1/.exec(value) + if (!quoted) { + return value + } + return quoted[2]?.replaceAll(/\\(["'\\])/g, '$1') ?? '' +} diff --git a/packages/postcss/src/css-macro/postcss.ts b/packages/postcss/src/css-macro/postcss.ts new file mode 100644 index 000000000..3ed8dcea0 --- /dev/null +++ b/packages/postcss/src/css-macro/postcss.ts @@ -0,0 +1,158 @@ +import type { AtRule, Helpers, PluginCreator, Rule } from 'postcss' +import { ifdef, ifdefAtRule, ifndef, ifndefAtRule, matchCustomPropertyFromValue, parseConditionalAtRuleParam } from './constants' + +const IFDEF_ENDIF_RE = /#(?:ifn?def|endif)/ +const CONDITIONAL_COMMENT_SPACING = ' ' +export const CSS_MACRO_POSTCSS_PLUGIN_NAME = 'postcss-weapp-tw-css-macro-plugin' + +export interface Options {} + +const creator: PluginCreator = () => { + return { + postcssPlugin: CSS_MACRO_POSTCSS_PLUGIN_NAME, + prepare() { + function replaceAtRuleWithConditionalComments( + atRule: AtRule, + helper: Helpers, + comment: ReturnType, + ) { + const hasPreviousNode = Boolean(atRule.prev()) + const clonedNodes = (atRule.nodes ?? []).map(node => node.clone()) + const startComment = helper.comment({ + raws: { + left: CONDITIONAL_COMMENT_SPACING, + right: CONDITIONAL_COMMENT_SPACING, + }, + text: comment.start, + }) + const endComment = helper.comment({ + raws: { + left: CONDITIONAL_COMMENT_SPACING, + right: CONDITIONAL_COMMENT_SPACING, + }, + text: comment.end, + }) + const nextNodes = [ + startComment, + ...clonedNodes, + endComment, + ] + atRule.replaceWith(nextNodes) + + startComment.raws.before = hasPreviousNode ? '\n' : '' + startComment.raws['after'] = '\n' + if (clonedNodes[0]) { + clonedNodes[0].raws.before = '\n' + } + endComment.raws.before = '\n' + endComment.raws['after'] = '\n' + + const nextNode = endComment?.next() + if (nextNode) { + nextNode.raws.before = '\n' + } + } + + function replaceNestedAtRuleWithConditionalRule( + atRule: AtRule, + helper: Helpers, + comment: ReturnType, + ) { + if (atRule.parent?.type !== 'rule') { + return false + } + + const parentRule = atRule.parent as Rule + const clonedNodes = (atRule.nodes ?? []).map(node => node.clone()) + const conditionalRule = parentRule.clone() + conditionalRule.removeAll() + conditionalRule.append(...clonedNodes) + + const startComment = helper.comment({ + raws: { + left: CONDITIONAL_COMMENT_SPACING, + right: CONDITIONAL_COMMENT_SPACING, + }, + text: comment.start, + }) + const endComment = helper.comment({ + raws: { + left: CONDITIONAL_COMMENT_SPACING, + right: CONDITIONAL_COMMENT_SPACING, + }, + text: comment.end, + }) + const nextNodes = [ + startComment, + conditionalRule, + endComment, + ] + const hasPreviousNode = Boolean(parentRule.prev()) + + atRule.remove() + if ((parentRule.nodes?.length ?? 0) === 0) { + parentRule.replaceWith(nextNodes) + } + else { + parentRule.after(nextNodes) + } + + startComment.raws.before = hasPreviousNode ? '\n' : '' + startComment.raws['after'] = '\n' + conditionalRule.raws.before = '\n' + endComment.raws.before = '\n' + endComment.raws['after'] = '\n' + + const nextNode = endComment.next() + if (nextNode) { + nextNode.raws.before = '\n' + } + + return true + } + + return { + AtRule(atRule, helper) { + if (atRule.name === ifdefAtRule || atRule.name === ifndefAtRule) { + const text = parseConditionalAtRuleParam(atRule.params) + const comment = atRule.name === ifndefAtRule ? ifndef(text) : ifdef(text) + if (replaceNestedAtRuleWithConditionalRule(atRule, helper, comment)) { + return + } + replaceAtRuleWithConditionalComments(atRule, helper, comment) + return + } + + if (atRule.name === 'media') { + const values: string[] = [] + matchCustomPropertyFromValue(atRule.params, (arr) => { + const value = arr[1] + if (value) { + values.push(value) + } + }) + if (values.length > 0) { + const isNegative = atRule.params.includes('not') + const text = values.join(' ') + const comment = isNegative ? ifndef(text) : ifdef(text) + if (replaceNestedAtRuleWithConditionalRule(atRule, helper, comment)) { + return + } + replaceAtRuleWithConditionalComments(atRule, helper, comment) + } + } + }, + CommentExit(comment) { + if (IFDEF_ENDIF_RE.test(comment.text)) { + comment.raws.left = CONDITIONAL_COMMENT_SPACING + comment.raws.right = CONDITIONAL_COMMENT_SPACING + } + }, + } + }, + } +} + +creator.postcss = true + +export default creator diff --git a/packages/weapp-tailwindcss/src/postcss/config-directive.ts b/packages/postcss/src/generator-plugin/config-directive.ts similarity index 100% rename from packages/weapp-tailwindcss/src/postcss/config-directive.ts rename to packages/postcss/src/generator-plugin/config-directive.ts diff --git a/packages/weapp-tailwindcss/src/postcss/context.ts b/packages/postcss/src/generator-plugin/context.ts similarity index 90% rename from packages/weapp-tailwindcss/src/postcss/context.ts rename to packages/postcss/src/generator-plugin/context.ts index 050731b19..582cc0998 100644 --- a/packages/weapp-tailwindcss/src/postcss/context.ts +++ b/packages/postcss/src/generator-plugin/context.ts @@ -1,6 +1,5 @@ import type { Result, Root } from 'postcss' -import type { WeappTailwindcssGenerateResult } from '../generator' -import type { WeappTailwindcssPostcssPluginOptions } from '../postcss' +import type { WeappTailwindcssPostcssGenerateResult, WeappTailwindcssPostcssPluginOptions } from './types' import path from 'node:path' import process from 'node:process' import postcss from 'postcss' @@ -46,7 +45,7 @@ export function replaceRootCss(root: Root, css: string, result: Result) { } } -export function addDependencyMessages(result: Result, generated: WeappTailwindcssGenerateResult) { +export function addDependencyMessages(result: Result, generated: WeappTailwindcssPostcssGenerateResult) { for (const file of generated.dependencies) { result.messages.push({ type: 'dependency', diff --git a/packages/postcss/src/generator-plugin/directives.ts b/packages/postcss/src/generator-plugin/directives.ts new file mode 100644 index 000000000..87b05aff9 --- /dev/null +++ b/packages/postcss/src/generator-plugin/directives.ts @@ -0,0 +1,46 @@ +import type { Root } from 'postcss' + +const TAILWIND_ROOT_DIRECTIVE_NAMES = new Set([ + 'config', + 'custom-variant', + 'plugin', + 'source', + 'tailwind', + 'theme', + 'utility', + 'variant', +]) + +function parseImportRequest(params: string) { + const match = /^(?:url\(\s*)?(["']?)([^"')\s]+)\1\s*\)?/.exec(params.trim()) + return match?.[2] +} + +function isTailwindImportRequest(request: string | undefined, options: { importFallback?: boolean | undefined } = {}) { + const normalized = options.importFallback && (request === 'weapp-tailwindcss' || request?.startsWith('weapp-tailwindcss/')) + ? request.replace(/^weapp-tailwindcss/, 'tailwindcss') + : request + return normalized === 'tailwindcss' + || normalized === 'tailwindcss4' + || normalized?.startsWith('tailwindcss/') === true + || normalized?.startsWith('tailwindcss4/') === true +} + +export function hasTailwindApplyDirective(css: string) { + return /@apply\b/.test(css) +} + +export function hasTailwindRootDirectives(root: Root, options: { importFallback?: boolean | undefined } = {}) { + let found = false + root.walkAtRules((rule) => { + if (rule.name === 'import' && isTailwindImportRequest(parseImportRequest(rule.params), options)) { + found = true + return false + } + if (TAILWIND_ROOT_DIRECTIVE_NAMES.has(rule.name)) { + found = true + return false + } + }) + return found +} diff --git a/packages/postcss/src/generator-plugin/index.ts b/packages/postcss/src/generator-plugin/index.ts new file mode 100644 index 000000000..8da982ab1 --- /dev/null +++ b/packages/postcss/src/generator-plugin/index.ts @@ -0,0 +1,176 @@ +import type { PluginCreator, Rule } from 'postcss' +import type { WeappTailwindcssPostcssPluginAdapters, WeappTailwindcssPostcssPluginOptions } from './types' +import postcss from 'postcss' +import { prependConfigDirective } from './config-directive' +import { addDependencyMessages, addSourceDependencyMessages, replaceRootCss, resolvePostcssBase, resolvePostcssProjectRoot } from './context' +import { hasTailwindApplyDirective, hasTailwindRootDirectives } from './directives' +import { collectAutoTailwindCandidates, collectPostcssLocalSources } from './source-files' +import { resolvePostcssTailwindVersion } from './tailwind-version' + +const PLUGIN_NAME = 'weapp-tailwindcss' + +function isTailwindV4ApplyOnlyCss(css: string, root: postcss.Root) { + return hasTailwindApplyDirective(css) + && !hasTailwindRootDirectives(root, { importFallback: true }) +} + +function resolveTailwindV4PostcssSourceCss(css: string, sourceOptions: Pick, root: postcss.Root) { + return isTailwindV4ApplyOnlyCss(css, root) + ? `@import "${sourceOptions.packageName ?? 'tailwindcss'}" source(none);\n${css}` + : css +} + +function normalizeSelector(selector: string) { + return selector.replace(/:not\(#\\#\)/g, '').trim() +} + +function collectApplyOnlyCssSelectors(css: string) { + const selectors = new Set() + try { + const root = postcss.parse(css) + root.walkRules((rule) => { + if (!rule.nodes?.some(node => node.type === 'atrule' && node.name === 'apply')) { + return + } + for (const selector of rule.selectors ?? [rule.selector]) { + const normalized = normalizeSelector(selector) + if (normalized) { + selectors.add(normalized) + } + } + }) + } + catch { + } + return selectors +} + +function ruleMatchesApplyOnlySelector(rule: Rule, selectors: Set) { + const ruleSelectors = rule.selectors ?? [rule.selector] + return ruleSelectors.some(selector => selectors.has(normalizeSelector(selector))) +} + +function filterApplyOnlyGeneratedCss(css: string, rawCss: string) { + const selectors = collectApplyOnlyCssSelectors(rawCss) + if (selectors.size === 0) { + return css + } + + try { + const root = postcss.parse(css) + root.walkRules((rule) => { + if (!ruleMatchesApplyOnlySelector(rule, selectors) && !rule.nodes?.some(node => node.type === 'decl' && node.prop.startsWith('--'))) { + rule.remove() + } + }) + root.walkAtRules((rule) => { + if (rule.nodes !== undefined && rule.nodes.length === 0) { + rule.remove() + } + }) + return root.toString() + } + catch { + return css + } +} + +export function createWeappTailwindcssPostcssPlugin( + adapters: WeappTailwindcssPostcssPluginAdapters, +): PluginCreator { + const plugin: PluginCreator = (options = {}) => { + return { + postcssPlugin: PLUGIN_NAME, + async Once(root, { result }) { + const { + candidates, + generator: userGeneratorOptions, + scanSources, + sources, + styleOptions, + ...sourceOptions + } = options + const generatorOptions = adapters.normalizeGeneratorOptions(userGeneratorOptions) + const tailwindVersion = resolvePostcssTailwindVersion(root, result, options) + + const [collectedSources, autoCandidates] = await Promise.all([ + collectPostcssLocalSources(root, result, options), + collectAutoTailwindCandidates(root, result, options), + ]) + const generatorConfig = generatorOptions.config ?? options.config + const rawCss = sourceOptions.css ?? root.toString() + const isApplyOnlyTailwindV4Css = tailwindVersion === 4 && isTailwindV4ApplyOnlyCss(rawCss, root) + const source = tailwindVersion === 3 + ? await adapters.resolveTailwindV3Source({ + config: generatorConfig, + css: rawCss, + base: resolvePostcssBase(result, options), + cwd: resolvePostcssProjectRoot(result, options), + projectRoot: resolvePostcssProjectRoot(result, options), + packageName: options.packageName, + postcssPlugin: options.postcssPlugin, + }) + : await adapters.resolveTailwindV4Source({ + ...sourceOptions, + css: prependConfigDirective( + resolveTailwindV4PostcssSourceCss(rawCss, sourceOptions, root), + generatorConfig, + ), + base: resolvePostcssBase(result, options), + projectRoot: resolvePostcssProjectRoot(result, options), + }) + const generator = adapters.createGenerator(source) + const generated = await generator.generate({ + candidates: new Set([ + ...autoCandidates, + ...(candidates ?? []), + ]), + scanSources: scanSources ?? false, + sources: [ + ...collectedSources.sources, + ...(sources ?? []), + ], + styleOptions: { + ...generatorOptions.styleOptions, + ...styleOptions, + }, + tailwindcssV3Compatibility: generatorOptions.tailwindcssV3Compatibility, + target: generatorOptions.target, + }) + const css = isApplyOnlyTailwindV4Css + ? filterApplyOnlyGeneratedCss(generated.css, rawCss) + : generated.css + + replaceRootCss(root, css, result) + addDependencyMessages(result, generated) + addSourceDependencyMessages(result, collectedSources.files) + result.messages.push({ + type: 'weapp-tailwindcss:generated', + plugin: PLUGIN_NAME, + target: generated.target, + classSet: generated.classSet, + rawCss: generated.rawCss, + }) + }, + } + } + + plugin.postcss = true + return plugin +} + +export type { + NormalizedWeappTailwindcssPostcssGeneratorOptions, + TailwindCandidateSource, + TailwindResolvedSource, + TailwindV3SourceOptions, + TailwindV4SourceOptions, + WeappTailwindcssPostcssGenerateOptions, + WeappTailwindcssPostcssGenerateResult, + WeappTailwindcssPostcssGenerator, + WeappTailwindcssPostcssGeneratorUserOptions, + WeappTailwindcssPostcssPluginAdapters, + WeappTailwindcssPostcssPluginOptions, + WeappTailwindcssPostcssTailwindVersion, + WeappTailwindcssPostcssTarget, +} from './types' diff --git a/packages/postcss/src/generator-plugin/package-version.ts b/packages/postcss/src/generator-plugin/package-version.ts new file mode 100644 index 000000000..6aad16b95 --- /dev/null +++ b/packages/postcss/src/generator-plugin/package-version.ts @@ -0,0 +1,85 @@ +import type { WeappTailwindcssPostcssTailwindVersion } from './types' +import { existsSync, readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +export const DEFAULT_TAILWINDCSS_GENERATOR_MAJOR_VERSION: WeappTailwindcssPostcssTailwindVersion = 4 + +function normalizeSupportedTailwindcssMajorVersion( + version: number | undefined, +): WeappTailwindcssPostcssTailwindVersion | undefined { + return version === 3 || version === 4 ? version : undefined +} + +interface PackageJsonLike { + name?: string + dependencies?: Record + devDependencies?: Record + peerDependencies?: Record + optionalDependencies?: Record +} + +function readPackageJson(packageJsonPath: string): PackageJsonLike | undefined { + try { + return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJsonLike + } + catch { + return undefined + } +} + +function readDeclaredPackageVersion(packageName: string, pkg: PackageJsonLike | undefined) { + return pkg?.dependencies?.[packageName] + ?? pkg?.devDependencies?.[packageName] + ?? pkg?.peerDependencies?.[packageName] + ?? pkg?.optionalDependencies?.[packageName] +} + +function findPackageJsonDeclaringPackage(packageName: string, base: string) { + let current = path.resolve(base) + while (true) { + const pkgPath = path.join(current, 'package.json') + if (existsSync(pkgPath)) { + const pkg = readPackageJson(pkgPath) + if (readDeclaredPackageVersion(packageName, pkg)) { + return pkgPath + } + if (pkg?.name !== 'weapp-tailwindcss') { + return undefined + } + } + const parent = path.dirname(current) + if (parent === current) { + return undefined + } + current = parent + } +} + +function readDeclaredPackageMajorVersion(version: string | undefined) { + const match = version?.match(/(?:^|\D)([34])(?:\.|\b)/) + return normalizeSupportedTailwindcssMajorVersion(match ? Number(match[1]) : undefined) +} + +export function readInstalledPackageMajorVersion( + packageName: string, + base: string, +): WeappTailwindcssPostcssTailwindVersion | undefined { + const packageJsonPath = findPackageJsonDeclaringPackage(packageName, base) + if (!packageJsonPath) { + return undefined + } + const declaredVersion = readDeclaredPackageVersion(packageName, readPackageJson(packageJsonPath)) + if (!declaredVersion) { + return undefined + } + try { + const require = createRequire(packageJsonPath) + const pkg = require(`${packageName}/package.json`) as { version?: string } + const major = Number(pkg.version?.split('.')[0]) + return normalizeSupportedTailwindcssMajorVersion(major) + } + catch { + return readDeclaredPackageMajorVersion(declaredVersion) + } +} diff --git a/packages/weapp-tailwindcss/src/postcss/source-files.ts b/packages/postcss/src/generator-plugin/source-files.ts similarity index 92% rename from packages/weapp-tailwindcss/src/postcss/source-files.ts rename to packages/postcss/src/generator-plugin/source-files.ts index e30ae877f..1ecb3cb32 100644 --- a/packages/weapp-tailwindcss/src/postcss/source-files.ts +++ b/packages/postcss/src/generator-plugin/source-files.ts @@ -1,12 +1,10 @@ import type { Result, Root } from 'postcss' -import type { TailwindV4CandidateSource } from '../generator' -import type { WeappTailwindcssPostcssPluginOptions } from '../postcss' -import type { TailwindSourceEntry } from '@/tailwindcss/source-scan' +import type { TailwindSourceEntry } from '../source-scan' +import type { TailwindCandidateSource, WeappTailwindcssPostcssPluginOptions } from './types' import { readFile } from 'node:fs/promises' import path from 'node:path' import { loadConfig } from 'tailwindcss-config' import { extractRawCandidatesWithPositions, extractValidCandidates } from 'tailwindcss-patch' -import { hasTailwindApplyDirective, hasTailwindRootDirectives } from '@/bundlers/shared/generator-css/directives' import { collectCssInlineSourceCandidates, createSourceScanPattern, @@ -15,15 +13,16 @@ import { normalizeLegacyContentEntries, parseConfigParam, resolveCssSourceEntries, -} from '@/tailwindcss/source-scan' +} from '../source-scan' import { resolvePostcssBase, resolvePostcssProjectRoot } from './context' +import { hasTailwindApplyDirective, hasTailwindRootDirectives } from './directives' const POSTCSS_SOURCE_PATTERN = createSourceScanPattern(DEFAULT_SOURCE_SCAN_EXTENSIONS) function isTailwindV4ApplyOnlyCss(root: Root, options: WeappTailwindcssPostcssPluginOptions) { return options.version === 4 && hasTailwindApplyDirective(root.toString()) - && !hasTailwindRootDirectives(root.toString(), { importFallback: true }) + && !hasTailwindRootDirectives(root, { importFallback: true }) } function getSourceExtension(file: string) { @@ -170,7 +169,7 @@ export async function collectPostcssLocalSources( ...await expandTailwindSourceEntries(sourceEntries), ...configContentFiles.files, ])] - const sources: TailwindV4CandidateSource[] = await Promise.all(files.map(async (file) => { + const sources: TailwindCandidateSource[] = await Promise.all(files.map(async (file) => { const extension = getSourceExtension(file) return { content: await readFile(file, 'utf8'), diff --git a/packages/weapp-tailwindcss/src/postcss/tailwind-version.ts b/packages/postcss/src/generator-plugin/tailwind-version.ts similarity index 90% rename from packages/weapp-tailwindcss/src/postcss/tailwind-version.ts rename to packages/postcss/src/generator-plugin/tailwind-version.ts index 68d4a031d..0a94059f6 100644 --- a/packages/weapp-tailwindcss/src/postcss/tailwind-version.ts +++ b/packages/postcss/src/generator-plugin/tailwind-version.ts @@ -1,7 +1,7 @@ import type { Result, Root } from 'postcss' -import type { WeappTailwindcssPostcssPluginOptions } from '../postcss' -import { DEFAULT_TAILWINDCSS_GENERATOR_MAJOR_VERSION, readInstalledPackageMajorVersion } from '../tailwindcss/version' +import type { WeappTailwindcssPostcssPluginOptions } from './types' import { resolvePostcssProjectRoot } from './context' +import { DEFAULT_TAILWINDCSS_GENERATOR_MAJOR_VERSION, readInstalledPackageMajorVersion } from './package-version' function hasTailwindV4CssSyntax(root: Root) { let hasV4Syntax = false diff --git a/packages/postcss/src/generator-plugin/types.ts b/packages/postcss/src/generator-plugin/types.ts new file mode 100644 index 000000000..3cd2f8a22 --- /dev/null +++ b/packages/postcss/src/generator-plugin/types.ts @@ -0,0 +1,114 @@ +import type { IStyleHandlerOptions } from '../types' + +export type WeappTailwindcssPostcssTarget = 'weapp' | 'web' | 'tailwind' +export type WeappTailwindcssPostcssTailwindVersion = 3 | 4 + +export interface TailwindCandidateSource { + content: string + extension?: string | undefined +} + +export interface WeappTailwindcssPostcssGenerateOptions { + candidates?: Iterable | undefined + scanSources?: boolean | undefined + sources?: TailwindCandidateSource[] | undefined + styleOptions?: Partial | undefined + tailwindcssV3Compatibility?: boolean | undefined + target?: WeappTailwindcssPostcssTarget | undefined +} + +export interface WeappTailwindcssPostcssGenerateResult { + css: string + rawCss: string + target: WeappTailwindcssPostcssTarget + classSet: Set + dependencies: string[] +} + +export interface WeappTailwindcssPostcssGenerator { + generate: (options?: WeappTailwindcssPostcssGenerateOptions) => Promise +} + +export interface TailwindV3SourceOptions { + projectRoot?: string | undefined + cwd?: string | undefined + base?: string | undefined + css?: string | undefined + config?: string | undefined + packageName?: string | undefined + postcssPlugin?: string | undefined +} + +export interface TailwindV4SourceOptions { + projectRoot?: string | undefined + base?: string | undefined + css?: string | undefined + packageName?: string | undefined +} + +export type TailwindResolvedSource = unknown + +export interface WeappTailwindcssPostcssGeneratorUserOptions { + target?: WeappTailwindcssPostcssTarget | undefined + config?: string | undefined + styleOptions?: Partial | undefined + importFallback?: boolean | undefined + tailwindcssV3Compatibility?: boolean | undefined + bareArbitraryValues?: unknown +} + +export interface NormalizedWeappTailwindcssPostcssGeneratorOptions { + target: WeappTailwindcssPostcssTarget + config?: string | undefined + styleOptions?: Partial | undefined + importFallback: boolean + tailwindcssV3Compatibility: boolean + bareArbitraryValues?: unknown +} + +export interface WeappTailwindcssPostcssPluginAdapters { + createGenerator: (source: TailwindResolvedSource) => WeappTailwindcssPostcssGenerator + normalizeGeneratorOptions: ( + options: WeappTailwindcssPostcssGeneratorUserOptions | undefined, + ) => NormalizedWeappTailwindcssPostcssGeneratorOptions + resolveTailwindV3Source: (options: TailwindV3SourceOptions) => Promise + resolveTailwindV4Source: (options: TailwindV4SourceOptions) => Promise +} + +/** + * `weapp-tailwindcss` PostCSS 插件配置。 + */ +export interface WeappTailwindcssPostcssPluginOptions extends TailwindV4SourceOptions { + /** + * 生成器配置,用于控制目标端、Tailwind 配置路径和 v4 兼容层。 + */ + generator?: WeappTailwindcssPostcssGeneratorUserOptions | undefined + /** + * 显式指定 Tailwind CSS 主版本。未传入时会从 CSS 与依赖环境推断。 + */ + version?: WeappTailwindcssPostcssTailwindVersion | undefined + /** + * Tailwind 配置文件路径。 + */ + config?: string | undefined + /** + * Tailwind PostCSS 插件名称。 + */ + postcssPlugin?: string | undefined + /** + * 额外传入的候选类名。 + */ + candidates?: Iterable | undefined + /** + * 是否扫描 Tailwind v4 源码入口中的候选类名。 + */ + scanSources?: WeappTailwindcssPostcssGenerateOptions['scanSources'] + /** + * 额外传入的 Tailwind v4 内联候选来源。 + */ + sources?: TailwindCandidateSource[] | undefined + /** + * 传给小程序 CSS 兼容转换器的额外配置。 + */ + styleOptions?: Partial | undefined +} diff --git a/packages/postcss/src/index.ts b/packages/postcss/src/index.ts index 7b2c65787..7ff7b0e6f 100644 --- a/packages/postcss/src/index.ts +++ b/packages/postcss/src/index.ts @@ -18,6 +18,31 @@ export { normalizeMiniProgramPrefixedDeclaration, removeUnsupportedMiniProgramPrefixedAtRule, } from './compat/mini-program-prefixes' +export { + compileCssMacroConditionalComments, + CSS_MACRO_STYLE_OPTIONS_MARKER, + hasCssMacroStyleOptions, + hasCssMacroTailwindV4Directive, + transformCssMacroCss, + withCssMacroStyleOptions, +} from './css-macro/auto' +export { CSS_MACRO_POSTCSS_PLUGIN_NAME, default as cssMacroPostcssPlugin } from './css-macro/postcss' +export { createWeappTailwindcssPostcssPlugin } from './generator-plugin' +export type { + NormalizedWeappTailwindcssPostcssGeneratorOptions, + TailwindCandidateSource, + TailwindResolvedSource, + TailwindV3SourceOptions, + TailwindV4SourceOptions, + WeappTailwindcssPostcssGenerateOptions, + WeappTailwindcssPostcssGenerateResult, + WeappTailwindcssPostcssGenerator, + WeappTailwindcssPostcssGeneratorUserOptions, + WeappTailwindcssPostcssPluginAdapters, + WeappTailwindcssPostcssPluginOptions, + WeappTailwindcssPostcssTailwindVersion, + WeappTailwindcssPostcssTarget, +} from './generator-plugin' export * from './handler' export { default as postcssHtmlTransform, type IOptions as PostcssHtmlTransformOptions } from './html-transform' export { diff --git a/packages/postcss/src/source-scan.ts b/packages/postcss/src/source-scan.ts new file mode 100644 index 000000000..11be8c36d --- /dev/null +++ b/packages/postcss/src/source-scan.ts @@ -0,0 +1,335 @@ +import type { Root } from 'postcss' +import { realpathSync } from 'node:fs' +import { stat } from 'node:fs/promises' +import path from 'node:path' +import micromatch from 'micromatch' +import { resolveProjectSourceFiles } from 'tailwindcss-patch' + +export interface TailwindSourceEntry { + base: string + pattern: string + negated: boolean +} + +export type { TailwindInlineSourceCandidates } from './source-scan/inline-source' +export { collectCssInlineSourceCandidates, expandInlineSourceCandidatePattern } from './source-scan/inline-source' + +interface LegacyContentObject { + files?: LegacyContentConfig + relative?: boolean +} + +type LegacyContentConfig + = | string + | string[] + | LegacyContentObject + | Array + +export const DEFAULT_SOURCE_SCAN_EXTENSIONS = [ + 'html', + 'wxml', + 'axml', + 'jxml', + 'ksml', + 'ttml', + 'qml', + 'tyml', + 'xhsml', + 'swan', + 'vue', + 'mpx', + 'js', + 'jsx', + 'ts', + 'tsx', +] + +export const FULL_SOURCE_SCAN_EXTENSIONS = [ + 'js', + 'jsx', + 'mjs', + 'cjs', + 'ts', + 'tsx', + 'mts', + 'cts', + 'vue', + 'uvue', + 'nvue', + 'svelte', + 'mpx', + 'html', + 'wxml', + 'axml', + 'jxml', + 'ksml', + 'ttml', + 'qml', + 'tyml', + 'xhsml', + 'swan', + 'css', + 'wxss', + 'acss', + 'jxss', + 'ttss', + 'qss', + 'tyss', + 'scss', + 'sass', + 'less', + 'styl', + 'stylus', +] + +export function createSourceScanPattern(extensions = DEFAULT_SOURCE_SCAN_EXTENSIONS) { + return `**/*.{${extensions.join(',')}}` +} + +export const FULL_SOURCE_SCAN_PATTERN = createSourceScanPattern(FULL_SOURCE_SCAN_EXTENSIONS) +export const FULL_SOURCE_SCAN_EXTENSION_RE = new RegExp(`\\.(?:${FULL_SOURCE_SCAN_EXTENSIONS.map(extension => extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})$`) + +export function toPosixPath(value: string) { + return value.split(path.sep).join('/') +} + +export function resolveSourceScanPath(value: string) { + const resolved = path.resolve(value) + try { + return realpathSync.native(resolved) + } + catch { + return resolved + } +} + +function normalizeEntryPattern(entry: TailwindSourceEntry) { + return path.isAbsolute(entry.pattern) + ? toPosixPath(path.relative(resolveSourceScanPath(entry.base), entry.pattern)) + : entry.pattern +} + +function isFileMatchedByTailwindSourceEntry(file: string, entry: TailwindSourceEntry) { + const relative = toPosixPath(path.relative(resolveSourceScanPath(entry.base), file)) + return relative && !relative.startsWith('../') && !path.isAbsolute(relative) && micromatch.isMatch(relative, normalizeEntryPattern(entry)) +} + +export function isFileExcludedByTailwindSourceEntries(file: string, entries: TailwindSourceEntry[] | undefined) { + if (!entries?.length) { + return false + } + const resolvedFile = resolveSourceScanPath(file) + return entries.some(entry => entry.negated && isFileMatchedByTailwindSourceEntry(resolvedFile, entry)) +} + +export function isFileMatchedByTailwindSourceEntries(file: string, entries: TailwindSourceEntry[] | undefined) { + if (!entries?.length) { + return true + } + const positiveEntries = entries.filter(entry => !entry.negated) + const negativeEntries = entries.filter(entry => entry.negated) + const resolvedFile = resolveSourceScanPath(file) + if (positiveEntries.length === 0) { + return !negativeEntries.some(entry => isFileMatchedByTailwindSourceEntry(resolvedFile, entry)) + } + const matchesPositive = positiveEntries.some(entry => isFileMatchedByTailwindSourceEntry(resolvedFile, entry)) + if (!matchesPositive) { + return false + } + return !negativeEntries.some(entry => isFileMatchedByTailwindSourceEntry(resolvedFile, entry)) +} + +export function createTailwindSourceEntryMatcher(entries: TailwindSourceEntry[] | undefined) { + if (!entries?.length) { + return undefined + } + return (file: string) => isFileMatchedByTailwindSourceEntries(file, entries) +} + +export function parseConfigParam(params: string) { + const value = params.trim() + const match = /^(['"])(.+)\1$/.exec(value) + return match?.[2] +} + +function isLegacyContentObject(value: unknown): value is LegacyContentObject { + return typeof value === 'object' && value !== null && 'files' in value +} + +function normalizeGlobPattern(pattern: string) { + return pattern.startsWith('./') ? pattern.slice(2) : pattern +} + +function hasGlobMagic(value: string) { + return /[*?[\]{}()!+@]/.test(value) +} + +function splitStaticGlobPrefix(pattern: string) { + const normalized = normalizeGlobPattern(pattern) + const segments = normalized.split(/[\\/]+/) + const prefix: string[] = [] + const rest: string[] = [] + let reachedGlob = false + for (const segment of segments) { + if (!reachedGlob && segment && !hasGlobMagic(segment)) { + prefix.push(segment) + continue + } + reachedGlob = true + rest.push(segment) + } + return { + prefix, + rest, + } +} + +export function normalizeLegacyContentEntries( + content: unknown, + base: string, + options: { relativeBase?: string | undefined } = {}, +): TailwindSourceEntry[] { + if (typeof content === 'string') { + const negated = content.startsWith('!') + return [{ + base, + negated, + pattern: normalizeGlobPattern(negated ? content.slice(1) : content), + }] + } + if (Array.isArray(content)) { + return content.flatMap(item => normalizeLegacyContentEntries(item, base, options)) + } + if (isLegacyContentObject(content)) { + return normalizeLegacyContentEntries(content.files, content.relative && options.relativeBase ? options.relativeBase : base, options) + } + return [] +} + +async function pathExistsAsDirectory(file: string) { + try { + return (await stat(file)).isDirectory() + } + catch { + return false + } +} + +export async function resolveTailwindSourceEntry( + sourcePath: string, + base: string, + negated: boolean, + defaultPattern = createSourceScanPattern(), +): Promise { + const absoluteSource = path.isAbsolute(sourcePath) ? path.resolve(sourcePath) : path.resolve(base, sourcePath) + if (await pathExistsAsDirectory(absoluteSource)) { + return { + base: absoluteSource, + negated, + pattern: normalizeGlobPattern(defaultPattern), + } + } + + if (path.isAbsolute(sourcePath)) { + return { + base: path.dirname(absoluteSource), + negated, + pattern: normalizeGlobPattern(path.basename(absoluteSource)), + } + } + + const { prefix, rest } = splitStaticGlobPrefix(sourcePath) + if (prefix.length > 0 && rest.length > 0) { + return { + base: path.resolve(base, ...prefix), + negated, + pattern: normalizeGlobPattern(rest.join('/')), + } + } + + return { + base, + negated, + pattern: normalizeGlobPattern(sourcePath), + } +} + +export function parseSourceFileParam(params: string) { + const value = params.trim() + if (!value || value === 'none' || value.startsWith('inline(')) { + return undefined + } + + const negated = value.startsWith('not ') + const sourceValue = negated ? value.slice(4).trim() : value + if (sourceValue.startsWith('inline(')) { + return undefined + } + + const match = /^(['"])(.+)\1$/.exec(sourceValue) + return match?.[2] + ? { + negated, + sourcePath: match[2], + } + : undefined +} + +export async function resolveCssSourceEntries( + root: Root, + base: string, + defaultPattern = createSourceScanPattern(), +) { + const entries: TailwindSourceEntry[] = [] + const tasks: Array> = [] + root.walkAtRules('source', (rule) => { + const parsed = parseSourceFileParam(rule.params) + if (!parsed) { + return + } + tasks.push(resolveTailwindSourceEntry(parsed.sourcePath, base, parsed.negated, defaultPattern)) + }) + entries.push(...await Promise.all(tasks)) + return entries +} + +export async function expandTailwindSourceEntries( + entries: TailwindSourceEntry[], + options: { + ignore?: string[] + } = {}, +) { + if (entries.length === 0) { + return [] + } + + const files = new Set() + const entriesByBase = new Map() + for (const entry of entries) { + const base = path.resolve(entry.base) + const group = entriesByBase.get(base) ?? [] + group.push({ + ...entry, + base, + }) + entriesByBase.set(base, group) + } + + await Promise.all([...entriesByBase.entries()].map(async ([base, group]) => { + const ignoredSources = options.ignore?.map(pattern => ({ + base, + pattern: normalizeGlobPattern(pattern), + negated: true, + })) + const matched = await resolveProjectSourceFiles({ + cwd: base, + sources: group, + ...(ignoredSources === undefined ? {} : { ignoredSources }), + }) + for (const file of matched) { + files.add(path.resolve(file)) + } + })) + + return [...files].filter(file => !isFileExcludedByTailwindSourceEntries(file, entries)) +} diff --git a/packages/postcss/src/source-scan/inline-source.ts b/packages/postcss/src/source-scan/inline-source.ts new file mode 100644 index 000000000..4f97b0d96 --- /dev/null +++ b/packages/postcss/src/source-scan/inline-source.ts @@ -0,0 +1,193 @@ +import type { Root } from 'postcss' + +export interface TailwindInlineSourceCandidates { + included: Set + excluded: Set +} + +const NUMERICAL_RANGE_RE = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/ + +function segmentTopLevel(input: string, separator: string, options: { keepEmpty?: boolean } = {}) { + const parts: string[] = [] + const stack: string[] = [] + let lastPos = 0 + let quote: string | undefined + for (let index = 0; index < input.length; index++) { + const char = input[index] + if (char === '\\') { + index += 1 + continue + } + if (quote) { + if (char === quote) { + quote = undefined + } + continue + } + if (char === '"' || char === '\'') { + quote = char + continue + } + if (char === '(') { + stack.push(')') + continue + } + if (char === '[') { + stack.push(']') + continue + } + if (char === '{') { + stack.push('}') + continue + } + if (stack.length > 0 && char === stack[stack.length - 1]) { + stack.pop() + continue + } + if (stack.length === 0 && char === separator) { + const part = input.slice(lastPos, index) + if (part || options.keepEmpty) { + parts.push(part) + } + lastPos = index + 1 + } + } + const part = input.slice(lastPos) + if (part || options.keepEmpty) { + parts.push(part) + } + return parts +} + +function isSequence(value: string) { + return NUMERICAL_RANGE_RE.test(value) +} + +function expandSequence(value: string) { + const match = value.match(NUMERICAL_RANGE_RE) + if (!match) { + return [value] + } + const [, start, end, stepValue] = match + if (start === undefined || end === undefined) { + return [value] + } + let step = stepValue ? Number.parseInt(stepValue, 10) : undefined + const startNumber = Number.parseInt(start, 10) + const endNumber = Number.parseInt(end, 10) + const increasing = startNumber < endNumber + if (step === undefined) { + step = increasing ? 1 : -1 + } + if (step === 0) { + return [] + } + if (increasing && step < 0) { + step = -step + } + if (!increasing && step > 0) { + step = -step + } + + const result: string[] = [] + for ( + let value = startNumber; + increasing ? value <= endNumber : value >= endNumber; + value += step + ) { + result.push(String(value)) + } + return result +} + +export function expandInlineSourceCandidatePattern(pattern: string): string[] { + const index = pattern.indexOf('{') + if (index === -1) { + return [pattern] + } + + const prefix = pattern.slice(0, index) + const rest = pattern.slice(index) + let depth = 0 + let endIndex = -1 + for (let index = 0; index < rest.length; index++) { + const char = rest[index] + if (char === '{') { + depth += 1 + } + else if (char === '}') { + depth -= 1 + if (depth === 0) { + endIndex = index + break + } + } + } + if (endIndex === -1) { + return [pattern] + } + + const inner = rest.slice(1, endIndex) + const suffix = rest.slice(endIndex + 1) + const parts = (isSequence(inner) ? expandSequence(inner) : segmentTopLevel(inner, ',', { keepEmpty: true })) + .flatMap(part => expandInlineSourceCandidatePattern(part)) + const suffixes = expandInlineSourceCandidatePattern(suffix) + return suffixes.flatMap(suffix => + parts.map(part => `${prefix}${part}${suffix}`)) +} + +function parseSourceInlineParam(params: string) { + let value = params.trim() + const negated = value.startsWith('not ') + if (negated) { + value = value.slice(4).trim() + } + if (!value.startsWith('inline(') || !value.endsWith(')')) { + return undefined + } + + const inlineValue = value.slice(7, -1).trim() + const match = /^(['"])([\s\S]*)\1$/.exec(inlineValue) + if (!match) { + return undefined + } + const source = match[2] + if (source === undefined) { + return undefined + } + return { + negated, + source, + } +} + +export function collectCssInlineSourceCandidates(root: Root): TailwindInlineSourceCandidates { + const included = new Set() + const excluded = new Set() + root.walkAtRules('source', (rule) => { + const parsed = parseSourceInlineParam(rule.params) + if (!parsed) { + return + } + const target = parsed.negated ? excluded : included + for (const source of segmentTopLevel(parsed.source, ' ')) { + const trimmed = source.trim() + if (!trimmed) { + continue + } + for (const candidate of expandInlineSourceCandidatePattern(trimmed)) { + const normalized = candidate.trim() + if (normalized) { + target.add(normalized) + } + } + } + }) + for (const candidate of excluded) { + included.delete(candidate) + } + return { + included, + excluded, + } +} diff --git a/packages/postcss/test/generator-plugin.test.ts b/packages/postcss/test/generator-plugin.test.ts new file mode 100644 index 000000000..7b8affe17 --- /dev/null +++ b/packages/postcss/test/generator-plugin.test.ts @@ -0,0 +1,95 @@ +import type { WeappTailwindcssPostcssPluginAdapters } from '@/generator-plugin' +import postcss from 'postcss' +import { createWeappTailwindcssPostcssPlugin } from '@/generator-plugin' + +function createAdapters(overrides: Partial = {}) { + const generate = vi.fn(async () => ({ + css: '.generated { color: red; }', + rawCss: '.generated { color: red; }', + target: 'weapp' as const, + classSet: new Set(['generated']), + dependencies: [], + })) + const adapters: WeappTailwindcssPostcssPluginAdapters = { + createGenerator: vi.fn(() => ({ generate })), + normalizeGeneratorOptions: vi.fn(options => ({ + target: options?.target ?? 'weapp', + config: options?.config, + styleOptions: options?.styleOptions, + importFallback: options?.importFallback ?? true, + tailwindcssV3Compatibility: options?.tailwindcssV3Compatibility ?? true, + bareArbitraryValues: options?.bareArbitraryValues, + })), + resolveTailwindV3Source: vi.fn(async options => ({ version: 3, ...options })), + resolveTailwindV4Source: vi.fn(async options => ({ version: 4, ...options })), + ...overrides, + } + return { + adapters, + generate, + } +} + +describe('generator postcss plugin factory', () => { + it('routes explicit v3 css through the injected v3 source resolver', async () => { + const { adapters, generate } = createAdapters() + const plugin = createWeappTailwindcssPostcssPlugin(adapters) + const result = await postcss([ + plugin({ + version: 3, + candidates: ['text-red-500'], + scanSources: false, + }), + ]).process('@tailwind utilities;', { + from: undefined, + }) + + expect(adapters.resolveTailwindV3Source).toHaveBeenCalledWith(expect.objectContaining({ + css: '@tailwind utilities;', + })) + expect(adapters.resolveTailwindV4Source).not.toHaveBeenCalled() + expect(generate).toHaveBeenCalledWith(expect.objectContaining({ + candidates: new Set(['text-red-500']), + target: 'weapp', + })) + expect(result.css).toContain('.generated') + expect(result.messages).toContainEqual(expect.objectContaining({ + type: 'weapp-tailwindcss:generated', + target: 'weapp', + })) + }) + + it('prepends Tailwind import for v4 apply-only css before resolving source', async () => { + const { adapters } = createAdapters({ + createGenerator: vi.fn(() => ({ + generate: vi.fn(async () => ({ + css: [ + '.card { display: flex; }', + '.unused { color: red; }', + ':root { --spacing: 0.25rem; }', + ].join('\n'), + rawCss: '', + target: 'weapp', + classSet: new Set(), + dependencies: [], + })), + })), + }) + const plugin = createWeappTailwindcssPostcssPlugin(adapters) + const result = await postcss([ + plugin({ + version: 4, + scanSources: false, + }), + ]).process('.card { @apply flex; }', { + from: undefined, + }) + + expect(adapters.resolveTailwindV4Source).toHaveBeenCalledWith(expect.objectContaining({ + css: expect.stringContaining('@import "tailwindcss" source(none);'), + })) + expect(result.css).toContain('.card') + expect(result.css).toContain('--spacing') + expect(result.css).not.toContain('.unused') + }) +}) diff --git a/packages/postcss/tsdown.config.mts b/packages/postcss/tsdown.config.mts index be40822e1..2d6aa5f89 100644 --- a/packages/postcss/tsdown.config.mts +++ b/packages/postcss/tsdown.config.mts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsdown' export default defineConfig({ - entry: ['src/index.ts', 'src/types.ts', 'src/html-transform.ts'], + entry: ['src/index.ts', 'src/types.ts', 'src/html-transform.ts', 'src/css-macro/postcss.ts'], shims: true, format: ['cjs', 'esm'], deps: { diff --git a/packages/postcss/vitest.config.ts b/packages/postcss/vitest.config.ts index 2be8be694..dbb203b80 100644 --- a/packages/postcss/vitest.config.ts +++ b/packages/postcss/vitest.config.ts @@ -6,6 +6,18 @@ const alias = [ find: '@', replacement: path.resolve(__dirname, './src'), }, + { + find: /^tailwindcss-config$/, + replacement: path.resolve(__dirname, '../tailwindcss-config/src/index.ts'), + }, + { + find: /^@weapp-tailwindcss\/shared$/, + replacement: path.resolve(__dirname, '../shared/src/index.ts'), + }, + { + find: /^@weapp-tailwindcss\/postcss-calc$/, + replacement: path.resolve(__dirname, '../postcss-calc/src/index.ts'), + }, ] export default defineProject({ diff --git a/packages/weapp-tailwindcss/src/css-macro/auto.ts b/packages/weapp-tailwindcss/src/css-macro/auto.ts index 5c6ea49ab..cf18d36ae 100644 --- a/packages/weapp-tailwindcss/src/css-macro/auto.ts +++ b/packages/weapp-tailwindcss/src/css-macro/auto.ts @@ -1,219 +1,18 @@ -import type { IStyleHandlerOptions, LoadedPostcssOptions } from '@weapp-tailwindcss/postcss/types' -import type { ResultPlugin } from 'postcss-load-config' -import process from 'node:process' -import postcss from 'postcss' -import cssMacroPostcssPlugin, { CSS_MACRO_POSTCSS_PLUGIN_NAME } from './postcss' +export { + compileCssMacroConditionalComments, + CSS_MACRO_STYLE_OPTIONS_MARKER, + hasCssMacroStyleOptions, + hasCssMacroTailwindV4Directive, + transformCssMacroCss, + withCssMacroStyleOptions, +} from '@weapp-tailwindcss/postcss' export const CSS_MACRO_PLUGIN_MARKER = '__weappTailwindcssCssMacro' -export const CSS_MACRO_STYLE_OPTIONS_MARKER = '__weappTailwindcssCssMacroEnabled' interface CssMacroMarkedPlugin { [CSS_MACRO_PLUGIN_MARKER]?: true } -type CssMacroStyleOptions = Partial & { - [CSS_MACRO_STYLE_OPTIONS_MARKER]?: true -} - -type ConditionalValue = boolean | undefined - -const PLATFORM_ENV_KEYS = [ - 'WEAPP_TW_TARGET', - 'WEAPP_TAILWINDCSS_TARGET', - 'UNI_PLATFORM', - 'UNI_UTS_PLATFORM', - 'TARO_ENV', - 'MPX_CLI_MODE', - 'MPX_CURRENT_TARGET_MODE', -] as const - -const CONDITIONAL_END_RE = /^\s*#endif\s*$/ - -function readEnvValue(key: string): string | undefined { - return typeof process === 'undefined' ? undefined : process.env[key] -} - -function normalizePlatformToken(value: string | undefined): string | undefined { - const normalized = value?.trim().replaceAll('_', '-').toUpperCase() - return normalized || undefined -} - -function resolveCssMacroPlatform(options: Pick | undefined): string | undefined { - const explicit = normalizePlatformToken(options?.platform) - if (explicit) { - return explicit - } - - for (const key of PLATFORM_ENV_KEYS) { - const value = normalizePlatformToken(readEnvValue(key)) - if (value) { - return value - } - } - - return undefined -} - -function createPlatformTokenSet(platform: string | undefined) { - const normalized = normalizePlatformToken(platform) - const tokens = new Set() - if (!normalized) { - return tokens - } - - tokens.add(normalized) - if (normalized.startsWith('MP-')) { - tokens.add('MP') - } - if (normalized === 'WEAPP' || normalized === 'WEIXIN' || normalized === 'WX') { - tokens.add('MP') - tokens.add('MP-WEIXIN') - } - if (normalized === 'MP-WEIXIN') { - tokens.add('WEAPP') - tokens.add('WEIXIN') - tokens.add('WX') - } - if (normalized === 'H5') { - tokens.add('WEB') - } - if (normalized === 'WEB') { - tokens.add('H5') - } - if (normalized === 'APP') { - tokens.add('APP-PLUS') - } - if (normalized.startsWith('APP-')) { - tokens.add('APP') - } - if (normalized.startsWith('QUICKAPP-WEBVIEW')) { - tokens.add('QUICKAPP-WEBVIEW') - } - return tokens -} - -function combineAnd(values: ConditionalValue[]): ConditionalValue { - if (values.includes(false)) { - return false - } - return values.every(value => value === true) ? true : undefined -} - -function combineOr(values: ConditionalValue[]): ConditionalValue { - if (values.includes(true)) { - return true - } - return values.every(value => value === false) ? false : undefined -} - -function evaluatePlatformExpression(expression: string, platformTokens: ReadonlySet): ConditionalValue { - const orParts = expression.split(/\s*\|\|\s*/) - const orValues = orParts.map((orPart) => { - const andParts = orPart.split(/\s*&&\s*/) - return combineAnd(andParts.map((part) => { - const token = normalizePlatformToken(part) - if (!token || /[<>=!()]/.test(token)) { - return undefined - } - return platformTokens.has(token) - })) - }) - return combineOr(orValues) -} - -function negateConditionalValue(value: ConditionalValue): ConditionalValue { - return value === undefined ? undefined : !value -} - -function getActiveConditionalValue(stack: ConditionalValue[]): ConditionalValue { - if (stack.includes(false)) { - return false - } - return stack.includes(undefined) ? undefined : true -} - -function parseConditionalStart(text: string) { - const normalized = text.trim() - if (!normalized.startsWith('#')) { - return undefined - } - - const body = normalized.slice(1).trimStart() - const directives = ['ifndef', 'ifdef'] as const - for (const directive of directives) { - if (!body.startsWith(directive)) { - continue - } - const expression = body.slice(directive.length).trim() - if (expression.length === 0) { - return undefined - } - return { - directive, - expression, - } - } - - return undefined -} - -export function compileCssMacroConditionalComments( - css: string, - options?: Pick, -): string { - const platform = resolveCssMacroPlatform(options) - const platformTokens = createPlatformTokenSet(platform) - if (platformTokens.size === 0 || !css.includes('#if')) { - return css - } - - try { - const root = postcss.parse(css) - const transformContainer = (container: postcss.Container) => { - const stack: ConditionalValue[] = [] - for (const node of [...container.nodes ?? []]) { - if (node.type === 'comment') { - const start = parseConditionalStart(node.text) - if (start) { - const value = start.directive === 'ifndef' - ? negateConditionalValue(evaluatePlatformExpression(start.expression, platformTokens)) - : evaluatePlatformExpression(start.expression, platformTokens) - const parentActive = getActiveConditionalValue(stack) - stack.push(value) - if (parentActive !== undefined && value !== undefined) { - node.remove() - } - continue - } - - if (CONDITIONAL_END_RE.test(node.text)) { - const value = stack.pop() - const parentActive = getActiveConditionalValue(stack) - if (parentActive !== undefined && value !== undefined) { - node.remove() - } - continue - } - } - - if (getActiveConditionalValue(stack) === false) { - node.remove() - continue - } - - if ('nodes' in node && node.nodes) { - transformContainer(node) - } - } - } - transformContainer(root) - return root.toString() - } - catch { - return css - } -} - export function markCssMacroPlugin(value: T): T { Object.defineProperty(value, CSS_MACRO_PLUGIN_MARKER, { configurable: false, @@ -246,100 +45,3 @@ export function hasCssMacroTailwindPlugin(plugins: unknown): boolean { return false } - -function parseCssPluginRequest(params: string) { - const value = params.trim() - const quoted = /^(['"])(.*?)\1/.exec(value) - if (quoted) { - return quoted[2] - } - - const url = /^url\(\s*(?:(['"])(.*?)\1|([^'")\s]+))\s*\)/.exec(value) - return url?.[2] ?? url?.[3] -} - -function isCssMacroPluginRequest(request: string | undefined) { - return request === 'weapp-tailwindcss/css-macro' -} - -export function hasCssMacroTailwindV4Directive(css: string | undefined): boolean { - if (!css?.includes('css-macro')) { - return false - } - - try { - let found = false - postcss.parse(css).walkAtRules('plugin', (rule) => { - if (isCssMacroPluginRequest(parseCssPluginRequest(rule.params))) { - found = true - } - }) - return found - } - catch { - return /@plugin\s+(?:url\(\s*)?["']weapp-tailwindcss\/css-macro["']/.test(css) - } -} - -function isCssMacroPostcssPlugin(plugin: unknown): boolean { - if (plugin === cssMacroPostcssPlugin) { - return true - } - return Boolean( - plugin - && (typeof plugin === 'function' || typeof plugin === 'object') - && (plugin as { postcssPlugin?: unknown }).postcssPlugin === CSS_MACRO_POSTCSS_PLUGIN_NAME, - ) -} - -function withCssMacroPostcssPlugins(plugins: unknown): LoadedPostcssOptions['plugins'] { - const macroPlugin = cssMacroPostcssPlugin() - - if (!plugins) { - return [macroPlugin] - } - - if (Array.isArray(plugins)) { - return plugins.some(isCssMacroPostcssPlugin) - ? plugins - : [...plugins, macroPlugin] - } - - if (typeof plugins === 'object') { - const values = Object.values(plugins as Record).filter(Boolean) as ResultPlugin[] - if (values.some(isCssMacroPostcssPlugin)) { - return values - } - return [...values, macroPlugin] - } - - return [macroPlugin] -} - -export function withCssMacroStyleOptions( - options: Partial | undefined, -): Partial { - const postcssOptions = options?.postcssOptions - return { - ...options, - [CSS_MACRO_STYLE_OPTIONS_MARKER]: true, - postcssOptions: { - ...postcssOptions, - plugins: withCssMacroPostcssPlugins(postcssOptions?.plugins), - }, - } as CssMacroStyleOptions -} - -export function hasCssMacroStyleOptions(options: Partial | undefined): boolean { - return Boolean((options as CssMacroStyleOptions | undefined)?.[CSS_MACRO_STYLE_OPTIONS_MARKER]) -} - -export async function transformCssMacroCss( - css: string, - options?: Pick, -): Promise { - const result = (await postcss([cssMacroPostcssPlugin()]).process(css, { - from: undefined, - })).css - return compileCssMacroConditionalComments(result, options) -} diff --git a/packages/weapp-tailwindcss/src/css-macro/postcss.ts b/packages/weapp-tailwindcss/src/css-macro/postcss.ts index 3ed8dcea0..185d84fe0 100644 --- a/packages/weapp-tailwindcss/src/css-macro/postcss.ts +++ b/packages/weapp-tailwindcss/src/css-macro/postcss.ts @@ -1,158 +1,5 @@ -import type { AtRule, Helpers, PluginCreator, Rule } from 'postcss' -import { ifdef, ifdefAtRule, ifndef, ifndefAtRule, matchCustomPropertyFromValue, parseConditionalAtRuleParam } from './constants' - -const IFDEF_ENDIF_RE = /#(?:ifn?def|endif)/ -const CONDITIONAL_COMMENT_SPACING = ' ' -export const CSS_MACRO_POSTCSS_PLUGIN_NAME = 'postcss-weapp-tw-css-macro-plugin' - -export interface Options {} - -const creator: PluginCreator = () => { - return { - postcssPlugin: CSS_MACRO_POSTCSS_PLUGIN_NAME, - prepare() { - function replaceAtRuleWithConditionalComments( - atRule: AtRule, - helper: Helpers, - comment: ReturnType, - ) { - const hasPreviousNode = Boolean(atRule.prev()) - const clonedNodes = (atRule.nodes ?? []).map(node => node.clone()) - const startComment = helper.comment({ - raws: { - left: CONDITIONAL_COMMENT_SPACING, - right: CONDITIONAL_COMMENT_SPACING, - }, - text: comment.start, - }) - const endComment = helper.comment({ - raws: { - left: CONDITIONAL_COMMENT_SPACING, - right: CONDITIONAL_COMMENT_SPACING, - }, - text: comment.end, - }) - const nextNodes = [ - startComment, - ...clonedNodes, - endComment, - ] - atRule.replaceWith(nextNodes) - - startComment.raws.before = hasPreviousNode ? '\n' : '' - startComment.raws['after'] = '\n' - if (clonedNodes[0]) { - clonedNodes[0].raws.before = '\n' - } - endComment.raws.before = '\n' - endComment.raws['after'] = '\n' - - const nextNode = endComment?.next() - if (nextNode) { - nextNode.raws.before = '\n' - } - } - - function replaceNestedAtRuleWithConditionalRule( - atRule: AtRule, - helper: Helpers, - comment: ReturnType, - ) { - if (atRule.parent?.type !== 'rule') { - return false - } - - const parentRule = atRule.parent as Rule - const clonedNodes = (atRule.nodes ?? []).map(node => node.clone()) - const conditionalRule = parentRule.clone() - conditionalRule.removeAll() - conditionalRule.append(...clonedNodes) - - const startComment = helper.comment({ - raws: { - left: CONDITIONAL_COMMENT_SPACING, - right: CONDITIONAL_COMMENT_SPACING, - }, - text: comment.start, - }) - const endComment = helper.comment({ - raws: { - left: CONDITIONAL_COMMENT_SPACING, - right: CONDITIONAL_COMMENT_SPACING, - }, - text: comment.end, - }) - const nextNodes = [ - startComment, - conditionalRule, - endComment, - ] - const hasPreviousNode = Boolean(parentRule.prev()) - - atRule.remove() - if ((parentRule.nodes?.length ?? 0) === 0) { - parentRule.replaceWith(nextNodes) - } - else { - parentRule.after(nextNodes) - } - - startComment.raws.before = hasPreviousNode ? '\n' : '' - startComment.raws['after'] = '\n' - conditionalRule.raws.before = '\n' - endComment.raws.before = '\n' - endComment.raws['after'] = '\n' - - const nextNode = endComment.next() - if (nextNode) { - nextNode.raws.before = '\n' - } - - return true - } - - return { - AtRule(atRule, helper) { - if (atRule.name === ifdefAtRule || atRule.name === ifndefAtRule) { - const text = parseConditionalAtRuleParam(atRule.params) - const comment = atRule.name === ifndefAtRule ? ifndef(text) : ifdef(text) - if (replaceNestedAtRuleWithConditionalRule(atRule, helper, comment)) { - return - } - replaceAtRuleWithConditionalComments(atRule, helper, comment) - return - } - - if (atRule.name === 'media') { - const values: string[] = [] - matchCustomPropertyFromValue(atRule.params, (arr) => { - const value = arr[1] - if (value) { - values.push(value) - } - }) - if (values.length > 0) { - const isNegative = atRule.params.includes('not') - const text = values.join(' ') - const comment = isNegative ? ifndef(text) : ifdef(text) - if (replaceNestedAtRuleWithConditionalRule(atRule, helper, comment)) { - return - } - replaceAtRuleWithConditionalComments(atRule, helper, comment) - } - } - }, - CommentExit(comment) { - if (IFDEF_ENDIF_RE.test(comment.text)) { - comment.raws.left = CONDITIONAL_COMMENT_SPACING - comment.raws.right = CONDITIONAL_COMMENT_SPACING - } - }, - } - }, - } -} - -creator.postcss = true - -export default creator +export { + CSS_MACRO_POSTCSS_PLUGIN_NAME, + default, + type Options, +} from '@weapp-tailwindcss/postcss/css-macro/postcss' diff --git a/packages/weapp-tailwindcss/src/postcss.ts b/packages/weapp-tailwindcss/src/postcss.ts index b0cdc6f1d..4f9368940 100644 --- a/packages/weapp-tailwindcss/src/postcss.ts +++ b/packages/weapp-tailwindcss/src/postcss.ts @@ -1,208 +1,25 @@ -import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types' -import type { PluginCreator, Rule } from 'postcss' import type { - TailwindV4CandidateSource, - TailwindV4SourceOptions, - WeappTailwindcssGenerateOptions, - WeappTailwindcssGeneratorUserOptions, -} from './generator' -import postcss from 'postcss' -import { hasTailwindApplyDirective, hasTailwindRootDirectives } from './bundlers/shared/generator-css/directives' + WeappTailwindcssPostcssPluginAdapters, + WeappTailwindcssPostcssPluginOptions, +} from '@weapp-tailwindcss/postcss' +import type { PluginCreator } from 'postcss' +import { createWeappTailwindcssPostcssPlugin } from '@weapp-tailwindcss/postcss' import { createWeappTailwindcssGenerator, normalizeWeappTailwindcssGeneratorOptions, resolveTailwindV3Source, resolveTailwindV4Source, } from './generator' -import { prependConfigDirective } from './postcss/config-directive' -import { addDependencyMessages, addSourceDependencyMessages, replaceRootCss, resolvePostcssBase, resolvePostcssProjectRoot } from './postcss/context' -import { collectAutoTailwindCandidates, collectPostcssLocalSources } from './postcss/source-files' -import { resolvePostcssTailwindVersion } from './postcss/tailwind-version' - -const PLUGIN_NAME = 'weapp-tailwindcss' - -function isTailwindV4ApplyOnlyCss(css: string) { - return hasTailwindApplyDirective(css) - && !hasTailwindRootDirectives(css, { importFallback: true }) -} - -function resolveTailwindV4PostcssSourceCss(css: string, sourceOptions: Pick) { - return isTailwindV4ApplyOnlyCss(css) - ? `@import "${sourceOptions.packageName ?? 'tailwindcss'}" source(none);\n${css}` - : css -} - -function normalizeSelector(selector: string) { - return selector.replace(/:not\(#\\#\)/g, '').trim() -} - -function collectApplyOnlyCssSelectors(css: string) { - const selectors = new Set() - try { - const root = postcss.parse(css) - root.walkRules((rule) => { - if (!rule.nodes?.some(node => node.type === 'atrule' && node.name === 'apply')) { - return - } - for (const selector of rule.selectors ?? [rule.selector]) { - const normalized = normalizeSelector(selector) - if (normalized) { - selectors.add(normalized) - } - } - }) - } - catch { - } - return selectors -} -function ruleMatchesApplyOnlySelector(rule: Rule, selectors: Set) { - const ruleSelectors = rule.selectors ?? [rule.selector] - return ruleSelectors.some(selector => selectors.has(normalizeSelector(selector))) -} - -function filterApplyOnlyGeneratedCss(css: string, rawCss: string) { - const selectors = collectApplyOnlyCssSelectors(rawCss) - if (selectors.size === 0) { - return css - } - - try { - const root = postcss.parse(css) - root.walkRules((rule) => { - if (!ruleMatchesApplyOnlySelector(rule, selectors) && !rule.nodes?.some(node => node.type === 'decl' && node.prop.startsWith('--'))) { - rule.remove() - } - }) - root.walkAtRules((rule) => { - if (rule.nodes !== undefined && rule.nodes.length === 0) { - rule.remove() - } - }) - return root.toString() - } - catch { - return css - } -} - -/** - * `weapp-tailwindcss` PostCSS 插件配置。 - */ -export interface WeappTailwindcssPostcssPluginOptions extends TailwindV4SourceOptions { - /** - * 生成器配置,用于控制目标端、Tailwind 配置路径和 v4 兼容层。 - */ - generator?: WeappTailwindcssGeneratorUserOptions - /** - * 显式指定 Tailwind CSS 主版本。未传入时会从 CSS 与依赖环境推断。 - */ - version?: 3 | 4 - /** - * Tailwind 配置文件路径。 - */ - config?: string - /** - * Tailwind PostCSS 插件名称。 - */ - postcssPlugin?: string - /** - * 额外传入的候选类名。 - */ - candidates?: Iterable - /** - * 是否扫描 Tailwind v4 源码入口中的候选类名。 - */ - scanSources?: WeappTailwindcssGenerateOptions['scanSources'] - /** - * 额外传入的 Tailwind v4 内联候选来源。 - */ - sources?: TailwindV4CandidateSource[] - /** - * 传给小程序 CSS 兼容转换器的额外配置。 - */ - styleOptions?: Partial +const adapters: WeappTailwindcssPostcssPluginAdapters = { + createGenerator: source => createWeappTailwindcssGenerator(source as Parameters[0]), + normalizeGeneratorOptions: options => normalizeWeappTailwindcssGeneratorOptions(options), + resolveTailwindV3Source, + resolveTailwindV4Source, } -export const weappTailwindcssPostcssPlugin: PluginCreator = (options = {}) => { - return { - postcssPlugin: PLUGIN_NAME, - async Once(root, { result }) { - const { - candidates, - generator: userGeneratorOptions, - scanSources, - sources, - styleOptions, - ...sourceOptions - } = options - const generatorOptions = normalizeWeappTailwindcssGeneratorOptions(userGeneratorOptions) - const tailwindVersion = resolvePostcssTailwindVersion(root, result, options) - - const [collectedSources, autoCandidates] = await Promise.all([ - collectPostcssLocalSources(root, result, options), - collectAutoTailwindCandidates(root, result, options), - ]) - const generatorConfig = generatorOptions.config ?? options.config - const rawCss = sourceOptions.css ?? root.toString() - const isApplyOnlyTailwindV4Css = tailwindVersion === 4 && isTailwindV4ApplyOnlyCss(rawCss) - const source = tailwindVersion === 3 - ? await resolveTailwindV3Source({ - config: generatorConfig, - css: rawCss, - base: resolvePostcssBase(result, options), - cwd: resolvePostcssProjectRoot(result, options), - projectRoot: resolvePostcssProjectRoot(result, options), - packageName: options.packageName, - postcssPlugin: options.postcssPlugin, - }) - : await resolveTailwindV4Source({ - ...sourceOptions, - css: prependConfigDirective( - resolveTailwindV4PostcssSourceCss(rawCss, sourceOptions), - generatorConfig, - ), - base: resolvePostcssBase(result, options), - projectRoot: resolvePostcssProjectRoot(result, options), - }) - const generator = createWeappTailwindcssGenerator(source) - const generateOptions: WeappTailwindcssGenerateOptions = { - candidates: new Set([ - ...autoCandidates, - ...(candidates ?? []), - ]), - scanSources: scanSources ?? false, - sources: [ - ...collectedSources.sources, - ...(sources ?? []), - ], - styleOptions: { - ...generatorOptions.styleOptions, - ...styleOptions, - }, - tailwindcssV3Compatibility: generatorOptions.tailwindcssV3Compatibility, - target: generatorOptions.target, - } - const generated = await generator.generate(generateOptions) - const css = isApplyOnlyTailwindV4Css - ? filterApplyOnlyGeneratedCss(generated.css, rawCss) - : generated.css - - replaceRootCss(root, css, result) - addDependencyMessages(result, generated) - addSourceDependencyMessages(result, collectedSources.files) - result.messages.push({ - type: 'weapp-tailwindcss:generated', - plugin: PLUGIN_NAME, - target: generated.target, - classSet: generated.classSet, - rawCss: generated.rawCss, - }) - }, - } -} +export type { WeappTailwindcssPostcssPluginOptions } -weappTailwindcssPostcssPlugin.postcss = true +export const weappTailwindcssPostcssPlugin: PluginCreator = createWeappTailwindcssPostcssPlugin(adapters) export default weappTailwindcssPostcssPlugin diff --git a/packages/weapp-tailwindcss/test/css-macro/index.test.ts b/packages/weapp-tailwindcss/test/css-macro/index.test.ts index 3ab40a889..8e770fdbc 100644 --- a/packages/weapp-tailwindcss/test/css-macro/index.test.ts +++ b/packages/weapp-tailwindcss/test/css-macro/index.test.ts @@ -1,5 +1,8 @@ // import plugin from 'tailwindcss/plugin' import { getCss } from '#test/helpers/getTwCss' +import { mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' import postcss from 'postcss' // import twPlugin from '../../dist/css-macro' import twPlugin from '@/css-macro' @@ -8,14 +11,44 @@ import postcssPlugin from '@/css-macro/postcss' import { createTailwindV3Engine, resolveTailwindV3Source } from '@/tailwindcss/v3-engine' import { createTailwindV4Engine, resolveTailwindV4Source } from '@/tailwindcss/v4-engine' -const TAILWIND_V4_MACRO_CSS = ` -@plugin "weapp-tailwindcss/css-macro"; +let cssMacroPluginPath: string + +beforeAll(async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'weapp-tw-css-macro-')) + cssMacroPluginPath = path.join(dir, 'css-macro.mjs').replaceAll('\\', '/') + await writeFile( + cssMacroPluginPath, + [ + 'const quote = value => `"${String(value).replaceAll("\\\\", "\\\\\\\\").replaceAll("\\"", "\\\\\\"")}"`;', + 'const conditional = (name, value) => `@${name} ${quote(value)}{&}`;', + 'export default function cssMacro({ matchVariant, addVariant }) {', + ' if (typeof matchVariant === "function") {', + ' matchVariant("ifdef", value => conditional("weapp-tw-ifdef", value));', + ' matchVariant("ifndef", value => conditional("weapp-tw-ifndef", value));', + ' }', + ' if (typeof addVariant === "function") {', + ' for (const [name, config] of Object.entries({})) {', + ' const normalized = typeof config === "string" ? { value: config, negative: false } : config;', + ' addVariant(name, conditional(normalized.negative ? "weapp-tw-ifndef" : "weapp-tw-ifdef", normalized.value));', + ' }', + ' }', + '}', + '', + ].join('\n'), + 'utf8', + ) +}) + +function createTailwindV4MacroCss() { + return ` +@plugin "${cssMacroPluginPath}"; @theme default { --color-blue-500: oklch(62.3% 0.214 259.815); --color-red-500: oklch(63.7% 0.237 25.331); } @tailwind utilities; ` +} // not screen and (weapp-tw-platform:MP-WEIXIN) // not screen and (weapp-tw-platform:uniVersion > 3.9) @@ -137,7 +170,7 @@ describe('css-macro tailwindcss plugin', () => { it('auto enables postcss macro transform for tailwindcss v4 @plugin', async () => { const source = await resolveTailwindV4Source({ - css: TAILWIND_V4_MACRO_CSS, + css: createTailwindV4MacroCss(), base: process.cwd(), }) const engine = createTailwindV4Engine(source) @@ -158,7 +191,7 @@ describe('css-macro tailwindcss plugin', () => { it('compiles tailwindcss v4 css-macro comments by mini-program platform before final css output', async () => { const source = await resolveTailwindV4Source({ - css: TAILWIND_V4_MACRO_CSS, + css: createTailwindV4MacroCss(), base: process.cwd(), }) const engine = createTailwindV4Engine(source) @@ -183,7 +216,7 @@ describe('css-macro tailwindcss plugin', () => { it('auto enables postcss macro transform for tailwindcss v4 web target', async () => { const source = await resolveTailwindV4Source({ - css: TAILWIND_V4_MACRO_CSS, + css: createTailwindV4MacroCss(), base: process.cwd(), }) const engine = createTailwindV4Engine(source) @@ -205,7 +238,7 @@ describe('css-macro tailwindcss plugin', () => { it('compiles tailwindcss v4 css-macro comments by web platform before final css output', async () => { const source = await resolveTailwindV4Source({ - css: TAILWIND_V4_MACRO_CSS, + css: createTailwindV4MacroCss(), base: process.cwd(), }) const engine = createTailwindV4Engine(source) diff --git a/packages/weapp-tailwindcss/tsconfig.json b/packages/weapp-tailwindcss/tsconfig.json index 8f6051aff..0d454b0ec 100644 --- a/packages/weapp-tailwindcss/tsconfig.json +++ b/packages/weapp-tailwindcss/tsconfig.json @@ -19,6 +19,9 @@ "@weapp-tailwindcss/postcss/*": [ "../postcss/src/*" ], + "@weapp-tailwindcss/postcss/css-macro/postcss": [ + "../postcss/src/css-macro/postcss.ts" + ], "@weapp-tailwindcss/postcss/types": [ "../postcss/src/types.ts" ], diff --git a/packages/weapp-tailwindcss/vitest.config.ts b/packages/weapp-tailwindcss/vitest.config.ts index f5d172dc6..ef125da53 100644 --- a/packages/weapp-tailwindcss/vitest.config.ts +++ b/packages/weapp-tailwindcss/vitest.config.ts @@ -21,6 +21,26 @@ export default defineConfig({ find: /^@weapp-tailwindcss\/postcss$/, replacement: path.resolve(__dirname, '../postcss/src/index.ts'), }, + { + find: /^@weapp-tailwindcss\/postcss\/css-macro\/postcss$/, + replacement: path.resolve(__dirname, '../postcss/src/css-macro/postcss.ts'), + }, + { + find: /^tailwindcss-config$/, + replacement: path.resolve(__dirname, '../tailwindcss-config/src/index.ts'), + }, + { + find: /^@weapp-tailwindcss\/shared$/, + replacement: path.resolve(__dirname, '../shared/src/index.ts'), + }, + { + find: /^@weapp-tailwindcss\/shared\/node$/, + replacement: path.resolve(__dirname, '../shared/src/node.ts'), + }, + { + find: /^@weapp-tailwindcss\/logger$/, + replacement: path.resolve(__dirname, '../logger/src/index.ts'), + }, { find: /^@weapp-tailwindcss\/postcss-calc$/, replacement: path.resolve(__dirname, '../postcss-calc/src/index.ts'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be05701a8..8c332ad64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3776,6 +3776,9 @@ importers: lru-cache: specifier: 11.5.1 version: 11.5.1 + micromatch: + specifier: ^4.0.8 + version: 4.0.8 postcss: specifier: catalog:postcss85tilde version: 8.5.15 @@ -3794,6 +3797,12 @@ importers: postcss-value-parser: specifier: ^4.2.0 version: 4.2.0 + tailwindcss-config: + specifier: workspace:* + version: link:../tailwindcss-config + tailwindcss-patch: + specifier: catalog:tailwindcssPatch + version: 9.4.3(magicast@0.5.3)(tailwindcss@4.3.0) devDependencies: '@csstools/css-color-parser': specifier: ^4.1.1 From 9b190c39b693f7cdff1ccc4e377f7398b7b4ddd5 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 20:22:44 +0800 Subject: [PATCH 2/7] chore: tidy postcss package dependencies --- packages/postcss/package.json | 1 + packages/weapp-tailwindcss/package.json | 1 + pnpm-lock.yaml | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/packages/postcss/package.json b/packages/postcss/package.json index 36bece0e4..2b87f7abb 100644 --- a/packages/postcss/package.json +++ b/packages/postcss/package.json @@ -82,6 +82,7 @@ "lru-cache": "11.5.1", "micromatch": "^4.0.8", "postcss": "catalog:postcss85tilde", + "postcss-load-config": "^6.0.1", "postcss-pxtrans": "^1.0.4", "postcss-rem-to-responsive-pixel": "catalog:postcssRem", "postcss-rule-unit-converter": "^0.2.2", diff --git a/packages/weapp-tailwindcss/package.json b/packages/weapp-tailwindcss/package.json index cb7ac8c5b..c6f5fa280 100644 --- a/packages/weapp-tailwindcss/package.json +++ b/packages/weapp-tailwindcss/package.json @@ -213,6 +213,7 @@ "lru-cache": "11.5.1", "magic-string": "0.30.21", "micromatch": "^4.0.8", + "postcss": "catalog:postcss85tilde", "postcss-load-config": "^6.0.1", "postcss-selector-parser": "^7.1.2", "semver": "~7.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c332ad64..3bcc0f4a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3782,6 +3782,9 @@ importers: postcss: specifier: catalog:postcss85tilde version: 8.5.15 + postcss-load-config: + specifier: ^6.0.1 + version: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) postcss-pxtrans: specifier: ^1.0.4 version: 1.0.4(postcss@8.5.15) @@ -3999,6 +4002,9 @@ importers: micromatch: specifier: ^4.0.8 version: 4.0.8 + postcss: + specifier: catalog:postcss85tilde + version: 8.5.15 postcss-load-config: specifier: ^6.0.1 version: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) From be3bec99ea75796041deb7f08e67f8ac2255512d Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 20:42:01 +0800 Subject: [PATCH 3/7] chore: consolidate postcss dependencies --- packages/postcss/src/index.ts | 29 +++++++++ packages/postcss/src/postcss-config.ts | 61 +++++++++++++++++++ packages/postcss/src/postcss-runtime.ts | 17 ++++++ .../src/vite-css-rules.ts} | 2 +- packages/weapp-tailwindcss/package.json | 3 - .../src/bundlers/shared/css-source-trace.ts | 2 +- .../shared/generator-css/directives.ts | 2 +- .../shared/generator-css/legacy-compat.ts | 2 +- .../shared/generator-css/legacy-selectors.ts | 2 +- .../shared/generator-css/legacy-units.ts | 2 +- .../shared/generator-css/local-imports.ts | 2 +- .../shared/generator-css/source-resolver.ts | 2 +- .../bundlers/shared/generator-css/user-css.ts | 2 +- .../shared/generator-css/user-layer-order.ts | 2 +- .../vite/generate-bundle/css-output.ts | 2 +- .../src/bundlers/vite/index.ts | 4 +- .../vite/official-tailwind-plugins.ts | 37 ----------- .../src/bundlers/vite/postcss-config.ts | 25 -------- .../src/bundlers/vite/processed-css-assets.ts | 10 ++- .../bundlers/vite/source-scan/css-entries.ts | 2 +- .../weapp-tw-runtime-classset-loader.ts | 2 +- packages/weapp-tailwindcss/src/postcss.ts | 2 +- .../tailwindcss/source-scan/inline-source.ts | 2 +- .../src/tailwindcss/v3-engine/generator.ts | 2 +- .../v3-engine/generator/content.ts | 2 +- .../src/tailwindcss/v3-engine/miniprogram.ts | 3 +- .../src/tailwindcss/v3-engine/source.ts | 2 +- .../src/tailwindcss/v4-engine/generator.ts | 2 +- .../v4-engine/generator/css-compat.ts | 2 +- .../v4-engine/generator/scan-sources.ts | 2 +- .../src/tailwindcss/v4-engine/source.ts | 2 +- packages/weapp-tailwindcss/src/types/index.ts | 3 +- .../types/postcss-html-transform-shim.d.ts | 2 +- .../src/uni-app-x/style-asset/style-value.ts | 2 +- .../weapp-tailwindcss/test-d/core.test-d.ts | 2 +- .../test-d/css-macro-postcss.test-d.ts | 2 +- pnpm-lock.yaml | 9 --- 37 files changed, 145 insertions(+), 108 deletions(-) create mode 100644 packages/postcss/src/postcss-config.ts create mode 100644 packages/postcss/src/postcss-runtime.ts rename packages/{weapp-tailwindcss/src/bundlers/vite/processed-css-assets/css-rules.ts => postcss/src/vite-css-rules.ts} (99%) delete mode 100644 packages/weapp-tailwindcss/src/bundlers/vite/postcss-config.ts diff --git a/packages/postcss/src/index.ts b/packages/postcss/src/index.ts index 7ff7b0e6f..4adbbcc71 100644 --- a/packages/postcss/src/index.ts +++ b/packages/postcss/src/index.ts @@ -54,9 +54,38 @@ export { type StyleProcessingPipeline, } from './pipeline' export { createFallbackPlaceholderReplacer } from './plugins/post/specificity-cleaner' +export { + getPostcssPluginName, + removeTailwindPostcssPlugins, + resolveFilteredPostcssConfig, +} from './postcss-config' +export { postcss } from './postcss-runtime' +export type { + AcceptedPlugin, + AtRule, + Container, + Declaration, + Document, + Helpers, + Plugin, + PluginCreator, + Node as PostcssNode, + ProcessOptions, + Processor, + Result, + Root, + Rule, +} from './postcss-runtime' export { createInjectPreflight } from './preflight' export { internalCssSelectorReplacer } from './shared' export * from './types' +export { + containsCssAfterMinify, + filterExistingCssRules, + mergeCoveredCssRuleDeclarations, + mergeMiniProgramPreflightRuleDeclarations, + mergeMiniProgramThemeScopeRuleDeclarations, +} from './vite-css-rules' export { composeRules as unitConversionComposeRules, presets as unitConversionPresets, diff --git a/packages/postcss/src/postcss-config.ts b/packages/postcss/src/postcss-config.ts new file mode 100644 index 000000000..a97278826 --- /dev/null +++ b/packages/postcss/src/postcss-config.ts @@ -0,0 +1,61 @@ +import postcssrc from 'postcss-load-config' + +const tailwindPostcssPluginNames = new Set(['tailwindcss', '@tailwindcss/postcss']) + +export function getPostcssPluginName(plugin: unknown): string | undefined { + if (!plugin) { + return + } + if (typeof plugin === 'function' && 'postcss' in plugin) { + try { + return getPostcssPluginName(plugin()) + } + catch { + return + } + } + if (typeof plugin !== 'object' || !('postcssPlugin' in plugin)) { + return + } + const { postcssPlugin } = plugin as { postcssPlugin?: unknown } + return typeof postcssPlugin === 'string' ? postcssPlugin : undefined +} + +function isTailwindPostcssPlugin(plugin: unknown) { + const name = getPostcssPluginName(plugin) + return typeof name === 'string' && tailwindPostcssPluginNames.has(name) +} + +export function removeTailwindPostcssPlugins(plugins: unknown[]) { + let removed = 0 + for (let i = plugins.length - 1; i >= 0; i--) { + if (isTailwindPostcssPlugin(plugins[i])) { + plugins.splice(i, 1) + removed++ + } + } + return removed +} + +export async function resolveFilteredPostcssConfig(root: string) { + try { + const loaded = await postcssrc({}, root) + const plugins = Array.isArray(loaded.plugins) ? [...loaded.plugins] : [] + const removed = removeTailwindPostcssPlugins(plugins) + if (removed === 0) { + return + } + return { + options: loaded.options, + plugins, + removed, + } + } + catch (error) { + const message = error instanceof Error ? error.message : String(error) + if (message.includes('No PostCSS Config found')) { + return + } + throw error + } +} diff --git a/packages/postcss/src/postcss-runtime.ts b/packages/postcss/src/postcss-runtime.ts new file mode 100644 index 000000000..cdd9cb035 --- /dev/null +++ b/packages/postcss/src/postcss-runtime.ts @@ -0,0 +1,17 @@ +export type { + AcceptedPlugin, + AtRule, + Container, + Declaration, + Document, + Helpers, + Node, + Plugin, + PluginCreator, + ProcessOptions, + Processor, + Result, + Root, + Rule, +} from 'postcss' +export { default as postcss } from 'postcss' diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets/css-rules.ts b/packages/postcss/src/vite-css-rules.ts similarity index 99% rename from packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets/css-rules.ts rename to packages/postcss/src/vite-css-rules.ts index 86ff6f082..b57941cfd 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets/css-rules.ts +++ b/packages/postcss/src/vite-css-rules.ts @@ -1,6 +1,6 @@ import type { Node, Selector } from 'postcss-selector-parser' -import postcss from 'postcss' import selectorParser from 'postcss-selector-parser' +import { postcss } from './postcss-runtime' const MINI_PROGRAM_PREFLIGHT_SELECTOR_KEY = 'view,text,::after,::before' const MINI_PROGRAM_PREFLIGHT_SELECTOR_KEYS = new Set(['view', 'text', '::after', '::before']) diff --git a/packages/weapp-tailwindcss/package.json b/packages/weapp-tailwindcss/package.json index c6f5fa280..1b70f53ca 100644 --- a/packages/weapp-tailwindcss/package.json +++ b/packages/weapp-tailwindcss/package.json @@ -213,9 +213,6 @@ "lru-cache": "11.5.1", "magic-string": "0.30.21", "micromatch": "^4.0.8", - "postcss": "catalog:postcss85tilde", - "postcss-load-config": "^6.0.1", - "postcss-selector-parser": "^7.1.2", "semver": "~7.8.4", "tailwindcss-config": "workspace:*", "tailwindcss-patch": "catalog:tailwindcssPatch", diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/css-source-trace.ts b/packages/weapp-tailwindcss/src/bundlers/shared/css-source-trace.ts index e797933fa..81971937b 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/css-source-trace.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/css-source-trace.ts @@ -2,7 +2,7 @@ import type { TailwindSourceEntry } from '@/tailwindcss/source-scan' import type { InternalUserDefinedOptions } from '@/types' import path from 'node:path' import process from 'node:process' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { replaceWxml } from '@/wxml' export interface CssTokenSource { diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/directives.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/directives.ts index a13b36d90..1233c75cb 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/directives.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/directives.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { extractConfigRequestFromSource, extractTailwindDirectiveLines, diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-compat.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-compat.ts index 677aa0d2b..74d321ec2 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-compat.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-compat.ts @@ -2,7 +2,7 @@ import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types' import type { TailwindResolvedSource } from '@/generator' import type { InternalUserDefinedOptions } from '@/types' import { readFileSync } from 'node:fs' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { removeUnsupportedMiniProgramAtRules } from '../css-cleanup' import { removeTailwindSourceDirectives, resolveCssEntrySource } from './directives' import { collectDedupedPostTransformCompatCss, collectGeneratedSelectors, removeDuplicatedViteMarkers, removeGeneratedSelectorCompatCss } from './legacy-selectors' diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-selectors.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-selectors.ts index 39c9e6837..a7fdddd9c 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-selectors.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-selectors.ts @@ -1,4 +1,4 @@ -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { replaceWxml } from '@/wxml' import { VITE_MARKER_RE } from './markers' diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-units.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-units.ts index 443cd2cf4..7126d0d6c 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-units.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/legacy-units.ts @@ -1,4 +1,4 @@ -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { normalizeCompatSelectors } from './legacy-selectors' const CSS_LENGTH_UNIT_RE = /(?:^|[\s(,])[-+]?(?:\d+|\d*\.\d+)(?:px|rem)\b/i diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/local-imports.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/local-imports.ts index 61a8de3a5..7cbbb3d51 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/local-imports.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/local-imports.ts @@ -1,4 +1,4 @@ -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { parseImportRequest, removeTailwindSourceDirectives } from './directives' const REMOTE_IMPORT_RE = /^(?:https?:)?\/\//i diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/source-resolver.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/source-resolver.ts index 1e62030c4..83ff913af 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/source-resolver.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/source-resolver.ts @@ -7,7 +7,7 @@ import type { UndefinedOptional } from '@/utils/object' import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { splitCandidateTokens } from 'tailwindcss-patch' import { resolveTailwindV4EntriesFromCss } from '@/bundlers/vite/source-scan' import { diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-css.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-css.ts index b8d9f167b..82a0a1712 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-css.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-css.ts @@ -1,6 +1,6 @@ import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types' import type { InternalUserDefinedOptions } from '@/types' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { removeUnsupportedMiniProgramAtRules } from '../css-cleanup' import { hasTailwindApplyDirective, diff --git a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-layer-order.ts b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-layer-order.ts index 7d2987d57..39fdeecc5 100644 --- a/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-layer-order.ts +++ b/packages/weapp-tailwindcss/src/bundlers/shared/generator-css/user-layer-order.ts @@ -1,4 +1,4 @@ -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' const USER_LAYER_COMPONENTS_START = '/*! weapp-tailwindcss layer components start */' const USER_LAYER_COMPONENTS_END = '/*! weapp-tailwindcss layer components end */' diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/css-output.ts b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/css-output.ts index 700bedeeb..b875d1fa2 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/css-output.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/css-output.ts @@ -1,6 +1,6 @@ import type { InternalUserDefinedOptions } from '@/types' import path from 'node:path' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { normalizeOutputPathKey } from '../../shared/module-graph' import { isCSSRequest } from '../utils' diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/index.ts b/packages/weapp-tailwindcss/src/bundlers/vite/index.ts index 99b2694e4..ca6e150fa 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/index.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/index.ts @@ -8,6 +8,7 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' import process from 'node:process' import { logger } from '@weapp-tailwindcss/logger' +import { getPostcssPluginName, removeTailwindPostcssPlugins, resolveFilteredPostcssConfig } from '@weapp-tailwindcss/postcss' import postcssHtmlTransform from '@weapp-tailwindcss/postcss/html-transform' import { hasTailwindApplyDirective, normalizeTailwindConfigDirectives, normalizeTailwindSourceForGenerator } from '@/bundlers/shared/generator-css/directives' import { vitePluginName } from '@/constants' @@ -32,8 +33,7 @@ import { isSourceStyleRequest, stripRequestQuery } from '../shared/style-request import { createViteCssFinalizerOutputPlugin } from './css-finalizer' import { createGenerateBundleHook, resolveViteCssPipelineOutputFile } from './generate-bundle' import { createCssHandlerOptionsCache } from './generate-bundle/css-handler-options' -import { disableAndRemoveTailwindVitePlugins, getPostcssPluginName, removeTailwindPostcssPlugins, removeTailwindVitePlugins } from './official-tailwind-plugins' -import { resolveFilteredPostcssConfig } from './postcss-config' +import { disableAndRemoveTailwindVitePlugins, removeTailwindVitePlugins } from './official-tailwind-plugins' import { parseVueRequest } from './query' import { resolveImplicitAppTypeFromViteRoot } from './resolve-app-type' import { createRewriteCssImportsPlugins, hasVitePipelineTailwindGenerationDirective } from './rewrite-css-imports' diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/official-tailwind-plugins.ts b/packages/weapp-tailwindcss/src/bundlers/vite/official-tailwind-plugins.ts index 2fd554798..4fdf08b46 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/official-tailwind-plugins.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/official-tailwind-plugins.ts @@ -1,42 +1,5 @@ import type { Plugin } from 'vite' -const tailwindPostcssPluginNames = new Set(['tailwindcss', '@tailwindcss/postcss']) - -export function getPostcssPluginName(plugin: unknown): string | undefined { - if (!plugin) { - return - } - if (typeof plugin === 'function' && 'postcss' in plugin) { - try { - return getPostcssPluginName(plugin()) - } - catch { - return - } - } - if (typeof plugin !== 'object' || !('postcssPlugin' in plugin)) { - return - } - const { postcssPlugin } = plugin as { postcssPlugin?: unknown } - return typeof postcssPlugin === 'string' ? postcssPlugin : undefined -} - -function isTailwindPostcssPlugin(plugin: unknown) { - const name = getPostcssPluginName(plugin) - return typeof name === 'string' && tailwindPostcssPluginNames.has(name) -} - -export function removeTailwindPostcssPlugins(plugins: unknown[]) { - let removed = 0 - for (let i = plugins.length - 1; i >= 0; i--) { - if (isTailwindPostcssPlugin(plugins[i])) { - plugins.splice(i, 1) - removed++ - } - } - return removed -} - function isTailwindVitePlugin(plugin: unknown) { if (!plugin || typeof plugin !== 'object' || !('name' in plugin)) { return false diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/postcss-config.ts b/packages/weapp-tailwindcss/src/bundlers/vite/postcss-config.ts deleted file mode 100644 index 499608112..000000000 --- a/packages/weapp-tailwindcss/src/bundlers/vite/postcss-config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import postcssrc from 'postcss-load-config' -import { removeTailwindPostcssPlugins } from './official-tailwind-plugins' - -export async function resolveFilteredPostcssConfig(root: string) { - try { - const loaded = await postcssrc({}, root) - const plugins = Array.isArray(loaded.plugins) ? [...loaded.plugins] : [] - const removed = removeTailwindPostcssPlugins(plugins) - if (removed === 0) { - return - } - return { - options: loaded.options, - plugins, - removed, - } - } - catch (error) { - const message = error instanceof Error ? error.message : String(error) - if (message.includes('No PostCSS Config found')) { - return - } - throw error - } -} diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets.ts b/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets.ts index bdd7e7cd5..74d18dea9 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/processed-css-assets.ts @@ -1,12 +1,18 @@ import type { OutputAsset, OutputBundle } from 'rollup' import type { InternalUserDefinedOptions } from '@/types' +import { + containsCssAfterMinify, + filterExistingCssRules, + mergeCoveredCssRuleDeclarations, + mergeMiniProgramPreflightRuleDeclarations, + mergeMiniProgramThemeScopeRuleDeclarations, + postcss, +} from '@weapp-tailwindcss/postcss' import path from 'pathe' -import postcss from 'postcss' import { parseBundlerGeneratedCssMarkerBlocks, stripBundlerGeneratedCssMarkers } from '../shared/generated-css-marker' import { parseImportRequest, removeTailwindSourceDirectives } from '../shared/generator-css/directives' import { extractMarkedUserLayerComponentsCss, mergeMarkedUserLayerComponentsCss } from '../shared/generator-css/user-layer-order' import { normalizeOutputPathKey } from '../shared/module-graph' -import { containsCssAfterMinify, filterExistingCssRules, mergeCoveredCssRuleDeclarations, mergeMiniProgramPreflightRuleDeclarations, mergeMiniProgramThemeScopeRuleDeclarations } from './processed-css-assets/css-rules' interface CssAssetMarkerMatcher { (asset: OutputAsset, file?: string): boolean diff --git a/packages/weapp-tailwindcss/src/bundlers/vite/source-scan/css-entries.ts b/packages/weapp-tailwindcss/src/bundlers/vite/source-scan/css-entries.ts index 5dce2bdfb..dfc5cc7ae 100644 --- a/packages/weapp-tailwindcss/src/bundlers/vite/source-scan/css-entries.ts +++ b/packages/weapp-tailwindcss/src/bundlers/vite/source-scan/css-entries.ts @@ -4,8 +4,8 @@ import type { TailwindcssPatcherLike, UserDefinedOptions } from '@/types' import { existsSync, readFileSync } from 'node:fs' import { stat } from 'node:fs/promises' import path from 'node:path' +import { postcss } from '@weapp-tailwindcss/postcss' import fg from 'fast-glob' -import postcss from 'postcss' import { loadConfig } from 'tailwindcss-config' import { collectCssInlineSourceCandidates, diff --git a/packages/weapp-tailwindcss/src/bundlers/webpack/loaders/weapp-tw-runtime-classset-loader.ts b/packages/weapp-tailwindcss/src/bundlers/webpack/loaders/weapp-tw-runtime-classset-loader.ts index 5c1b7de7f..ced673a5f 100644 --- a/packages/weapp-tailwindcss/src/bundlers/webpack/loaders/weapp-tw-runtime-classset-loader.ts +++ b/packages/weapp-tailwindcss/src/bundlers/webpack/loaders/weapp-tw-runtime-classset-loader.ts @@ -2,7 +2,7 @@ import type webpack from 'webpack' import type { RuntimeLoaderWatchDependencies, WebpackRuntimeClassSetLoaderOptions } from './runtime-registry' import { Buffer } from 'node:buffer' import process from 'node:process' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { removeUnsupportedCascadeLayers } from '@/tailwindcss/remove-unsupported-css' import { getWebpackLoaderRuntime } from './runtime-registry' import { registerWebpackWatchContext, registerWebpackWatchFile } from './watch-dependencies' diff --git a/packages/weapp-tailwindcss/src/postcss.ts b/packages/weapp-tailwindcss/src/postcss.ts index 4f9368940..3f65c8449 100644 --- a/packages/weapp-tailwindcss/src/postcss.ts +++ b/packages/weapp-tailwindcss/src/postcss.ts @@ -1,8 +1,8 @@ import type { + PluginCreator, WeappTailwindcssPostcssPluginAdapters, WeappTailwindcssPostcssPluginOptions, } from '@weapp-tailwindcss/postcss' -import type { PluginCreator } from 'postcss' import { createWeappTailwindcssPostcssPlugin } from '@weapp-tailwindcss/postcss' import { createWeappTailwindcssGenerator, diff --git a/packages/weapp-tailwindcss/src/tailwindcss/source-scan/inline-source.ts b/packages/weapp-tailwindcss/src/tailwindcss/source-scan/inline-source.ts index 4f97b0d96..0e1d1f56a 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/source-scan/inline-source.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/source-scan/inline-source.ts @@ -1,4 +1,4 @@ -import type { Root } from 'postcss' +import type { Root } from '@weapp-tailwindcss/postcss' export interface TailwindInlineSourceCandidates { included: Set diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator.ts b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator.ts index f4260cf4e..7f10ee5d4 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator.ts @@ -7,7 +7,7 @@ import type { TailwindV3ResolvedSource, } from './types' import { createRequire } from 'node:module' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { extractSourceCandidates, isBareArbitraryValuesEnabled, diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator/content.ts b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator/content.ts index 4cc489e80..597c94f98 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator/content.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/generator/content.ts @@ -1,6 +1,6 @@ import type { Config } from 'tailwindcss' import type { TailwindV3CandidateSource, TailwindV3GenerateOptions, TailwindV3ResolvedSource } from '../types' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' interface LegacyContentObject { files?: unknown diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/miniprogram.ts b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/miniprogram.ts index 695235363..57370898a 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/miniprogram.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/miniprogram.ts @@ -1,7 +1,6 @@ import type { IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types' import type { TailwindV3GenerateTarget } from './types' -import { createStyleHandler } from '@weapp-tailwindcss/postcss' -import postcss from 'postcss' +import { createStyleHandler, postcss } from '@weapp-tailwindcss/postcss' import { hasCssMacroStyleOptions, transformCssMacroCss } from '@/css-macro/auto' import { pruneMiniProgramGeneratedCss } from '../miniprogram' diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/source.ts b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/source.ts index 73e368185..8a58bbe25 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/source.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v3-engine/source.ts @@ -3,7 +3,7 @@ import type { TailwindV3ResolvedSource, TailwindV3SourceOptions } from './types' import type { TailwindcssPatcherLike } from '@/types' import path from 'node:path' import process from 'node:process' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { loadConfig } from 'tailwindcss-config' import { resolveTailwindcssOptions } from '@/tailwindcss/patcher-options' import { omitUndefined } from '@/utils/object' diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator.ts b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator.ts index 52d98bcc9..f1e2024cc 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator.ts @@ -7,7 +7,7 @@ import type { TailwindV4ResolvedSource, } from './types' import fs from 'node:fs' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { createTailwindV4Engine as createPatchTailwindV4Engine, extractRawCandidates } from 'tailwindcss-patch' import { hasCssMacroTailwindV4Directive, withCssMacroStyleOptions } from '@/css-macro/auto' import { omitUndefined } from '@/utils/object' diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/css-compat.ts b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/css-compat.ts index a4687ec51..fca8f2434 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/css-compat.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/css-compat.ts @@ -1,5 +1,5 @@ import type { TailwindV4GenerateTarget, TailwindV4ResolvedSource } from '../types' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { applyTailwindV3CompatibilityCss } from '../tailwind-v3-compatibility' import { createTailwindV4DefaultColorThemeCss } from '../tailwind-v4-default-colors' diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/scan-sources.ts b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/scan-sources.ts index fa99f05b3..ec18b8f64 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/scan-sources.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/generator/scan-sources.ts @@ -1,6 +1,6 @@ import type { TailwindV4GenerateOptions, TailwindV4ResolvedSource, TailwindV4SourcePattern } from '../types' import path from 'node:path' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { resolveCssSourceEntries, resolveTailwindSourceEntry } from '@/tailwindcss/source-scan' type TailwindV4ResolvedScanSources = TailwindV4GenerateOptions['scanSources'] diff --git a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/source.ts b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/source.ts index 2fb8e0ac5..0a64eb941 100644 --- a/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/source.ts +++ b/packages/weapp-tailwindcss/src/tailwindcss/v4-engine/source.ts @@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' import process from 'node:process' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { resolveTailwindV4Source as resolvePatchTailwindV4Source, resolveTailwindV4SourceFromPatchOptions, diff --git a/packages/weapp-tailwindcss/src/types/index.ts b/packages/weapp-tailwindcss/src/types/index.ts index c84f0ab4a..0e966e274 100644 --- a/packages/weapp-tailwindcss/src/types/index.ts +++ b/packages/weapp-tailwindcss/src/types/index.ts @@ -1,7 +1,6 @@ import type { ParseError, ParserOptions } from '@babel/parser' -import type { CssPreflightOptions, IStyleHandlerOptions } from '@weapp-tailwindcss/postcss/types' +import type { CssPreflightOptions, Document, IStyleHandlerOptions, Result as PostcssResult, Root } from '@weapp-tailwindcss/postcss' import type { SourceMap } from 'magic-string' -import type { Document, Result as PostcssResult, Root } from 'postcss' import type { ILengthUnitsPatchOptions, TailwindcssPatcher } from 'tailwindcss-patch' import type { ICreateCacheReturnType } from '../cache' import type { ItemOrItemArray } from './base' diff --git a/packages/weapp-tailwindcss/src/types/postcss-html-transform-shim.d.ts b/packages/weapp-tailwindcss/src/types/postcss-html-transform-shim.d.ts index 9236f26d9..450133b5d 100644 --- a/packages/weapp-tailwindcss/src/types/postcss-html-transform-shim.d.ts +++ b/packages/weapp-tailwindcss/src/types/postcss-html-transform-shim.d.ts @@ -1,5 +1,5 @@ declare module '@weapp-tailwindcss/postcss/html-transform' { - import type { PluginCreator } from 'postcss' + import type { PluginCreator } from '@weapp-tailwindcss/postcss' export interface IOptions { /** 当前编译平台 */ diff --git a/packages/weapp-tailwindcss/src/uni-app-x/style-asset/style-value.ts b/packages/weapp-tailwindcss/src/uni-app-x/style-asset/style-value.ts index bc359f680..9696953db 100644 --- a/packages/weapp-tailwindcss/src/uni-app-x/style-asset/style-value.ts +++ b/packages/weapp-tailwindcss/src/uni-app-x/style-asset/style-value.ts @@ -1,5 +1,5 @@ import type { OutputChunk, SourceMap } from 'rollup' -import postcss from 'postcss' +import { postcss } from '@weapp-tailwindcss/postcss' import { splitCandidateTokens } from 'tailwindcss-patch' import { replaceWxml } from '@/wxml' diff --git a/packages/weapp-tailwindcss/test-d/core.test-d.ts b/packages/weapp-tailwindcss/test-d/core.test-d.ts index eb8434ea7..41d774870 100644 --- a/packages/weapp-tailwindcss/test-d/core.test-d.ts +++ b/packages/weapp-tailwindcss/test-d/core.test-d.ts @@ -1,4 +1,4 @@ -import type { Document, Result as PostcssResult, Root } from 'postcss' +import type { Document, Result as PostcssResult, Root } from '@weapp-tailwindcss/postcss' import type { JsHandlerResult, UserDefinedOptions } from 'weapp-tailwindcss/types' import { expectType } from 'tsd' import { createContext } from 'weapp-tailwindcss/core' diff --git a/packages/weapp-tailwindcss/test-d/css-macro-postcss.test-d.ts b/packages/weapp-tailwindcss/test-d/css-macro-postcss.test-d.ts index 922dfb82b..328973b4c 100644 --- a/packages/weapp-tailwindcss/test-d/css-macro-postcss.test-d.ts +++ b/packages/weapp-tailwindcss/test-d/css-macro-postcss.test-d.ts @@ -1,4 +1,4 @@ -import type { PluginCreator } from 'postcss' +import type { PluginCreator } from '@weapp-tailwindcss/postcss' import type { Options as PostcssCssMacroOptions } from 'weapp-tailwindcss/css-macro/postcss' import { expectAssignable } from 'tsd' import postcssCssMacro from 'weapp-tailwindcss/css-macro/postcss' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bcc0f4a8..d9bd1b693 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4002,15 +4002,6 @@ importers: micromatch: specifier: ^4.0.8 version: 4.0.8 - postcss: - specifier: catalog:postcss85tilde - version: 8.5.15 - postcss-load-config: - specifier: ^6.0.1 - version: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) - postcss-selector-parser: - specifier: ^7.1.2 - version: 7.1.2 semver: specifier: ~7.8.4 version: 7.8.4 From 9f59ba213bada50eb1a46b31401539162cc516e5 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 22:39:21 +0800 Subject: [PATCH 4/7] fix: stabilize postcss tests and e2e watch ci --- .github/workflows/e2e-watch.yml | 17 +++++++----- e2e/watch/taro-demo-dev.test.ts | 27 +++++++++++++++++-- .../src/generator-plugin/tailwind-version.ts | 6 ++--- .../postcss/test/generator-plugin.test.ts | 16 +++++++++++ .../cases/demo/extended.ts | 1 + .../src/watch-hmr-regression/web.ts | 13 ++++----- 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/.github/workflows/e2e-watch.yml b/.github/workflows/e2e-watch.yml index f13a709f4..8c558b0d5 100644 --- a/.github/workflows/e2e-watch.yml +++ b/.github/workflows/e2e-watch.yml @@ -187,21 +187,23 @@ jobs: - os: windows-latest runner_label: windows watch_case: taro-webpack-react-tailwindcss-v4 - round_profile: issue33 + round_profile: default watch_save_snapshots: '1' - timeout_minutes: 70 - watch_timeout_ms: '600000' + watch_web_only: '1' + timeout_minutes: 45 + watch_timeout_ms: '420000' watch_poll_ms: '40' - watch_command_timeout_ms: '1800000' + watch_command_timeout_ms: '960000' - os: windows-latest runner_label: windows watch_case: mpx-tailwindcss-v4 round_profile: default watch_save_snapshots: '1' - timeout_minutes: 60 - watch_timeout_ms: '600000' + watch_main_style_only: '1' + timeout_minutes: 35 + watch_timeout_ms: '240000' watch_poll_ms: '40' - watch_command_timeout_ms: '2400000' + watch_command_timeout_ms: '600000' runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout_minutes }} @@ -251,6 +253,7 @@ jobs: E2E_WATCH_MAX_ATTEMPTS: ${{ matrix.watch_max_attempts || '2' }} E2E_WATCH_ROUND_PROFILE: ${{ matrix.round_profile }} E2E_WATCH_WEB_ONLY: ${{ matrix.watch_web_only || '0' }} + E2E_WATCH_MAIN_STYLE_ONLY: ${{ matrix.watch_main_style_only || '0' }} E2E_WATCH_SAVE_SNAPSHOTS: ${{ matrix.watch_save_snapshots }} NODE_OPTIONS: --max-old-space-size=6144 run: pnpm e2e:watch diff --git a/e2e/watch/taro-demo-dev.test.ts b/e2e/watch/taro-demo-dev.test.ts index c18aec29e..891ab7c6c 100644 --- a/e2e/watch/taro-demo-dev.test.ts +++ b/e2e/watch/taro-demo-dev.test.ts @@ -20,6 +20,28 @@ const TARGETS = [ 'taro-webpack-vue3-tailwindcss-v4', ] const STABLE_AFTER_READY_MS = 8_000 +const DEFAULT_READY_TIMEOUT_MS = 180_000 +const DEFAULT_TEST_TIMEOUT_MS = 240_000 + +function toNumberEnv(name: string, fallback: number) { + const value = process.env[name] + if (!value) { + return fallback + } + const numeric = Number(value) + return Number.isFinite(numeric) ? numeric : fallback +} + +function resolveTaroDevReadyTimeoutMs() { + return Math.max(DEFAULT_READY_TIMEOUT_MS, toNumberEnv('E2E_WATCH_TIMEOUT_MS', DEFAULT_READY_TIMEOUT_MS)) +} + +function resolveTaroDevTestTimeoutMs() { + return Math.max( + DEFAULT_TEST_TIMEOUT_MS, + resolveTaroDevReadyTimeoutMs() + STABLE_AFTER_READY_MS + 60_000, + ) +} async function stopProcessTree(child: ReturnType) { const pid = child.pid @@ -41,6 +63,7 @@ async function stopProcessTree(child: ReturnType) { } async function expectDemoDevWatchReady(project: string) { + const readyTimeoutMs = resolveTaroDevReadyTimeoutMs() const cwd = `${ROOT}/demo/${project}` const child = spawnPnpm(['dev'], { cwd, @@ -87,7 +110,7 @@ async function expectDemoDevWatchReady(project: string) { try { const startedAt = Date.now() - while (Date.now() - startedAt < 180_000) { + while (Date.now() - startedAt < readyTimeoutMs) { if (ready) { break } @@ -144,6 +167,6 @@ describe('e2e watch taro demo dev entry', () => { for (const target of targets) { it(`keeps ${target} pnpm dev in watch mode`, async () => { await expectDemoDevWatchReady(target) - }, 240_000) + }, resolveTaroDevTestTimeoutMs()) } }) diff --git a/packages/postcss/src/generator-plugin/tailwind-version.ts b/packages/postcss/src/generator-plugin/tailwind-version.ts index 0a94059f6..aec551364 100644 --- a/packages/postcss/src/generator-plugin/tailwind-version.ts +++ b/packages/postcss/src/generator-plugin/tailwind-version.ts @@ -21,14 +21,14 @@ export function resolvePostcssTailwindVersion( result: Result, options: WeappTailwindcssPostcssPluginOptions, ) { + if (options.version) { + return options.version + } const packageName = options.packageName ?? 'tailwindcss' const installedVersion = readInstalledPackageMajorVersion(packageName, resolvePostcssProjectRoot(result, options)) if (installedVersion) { return installedVersion } - if (options.version) { - return options.version - } if (packageName === '@tailwindcss/postcss' || packageName.includes('tailwindcss4')) { return 4 } diff --git a/packages/postcss/test/generator-plugin.test.ts b/packages/postcss/test/generator-plugin.test.ts index 7b8affe17..3d413aaab 100644 --- a/packages/postcss/test/generator-plugin.test.ts +++ b/packages/postcss/test/generator-plugin.test.ts @@ -92,4 +92,20 @@ describe('generator postcss plugin factory', () => { expect(result.css).toContain('--spacing') expect(result.css).not.toContain('.unused') }) + + it('uses explicit version before installed package detection', async () => { + const { adapters } = createAdapters() + const plugin = createWeappTailwindcssPostcssPlugin(adapters) + await postcss([ + plugin({ + version: 4, + scanSources: false, + }), + ]).process('.card { @apply flex; }', { + from: import.meta.filename, + }) + + expect(adapters.resolveTailwindV4Source).toHaveBeenCalled() + expect(adapters.resolveTailwindV3Source).not.toHaveBeenCalled() + }) }) diff --git a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts index 749487253..1264a6405 100644 --- a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts +++ b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts @@ -1061,6 +1061,7 @@ export function buildDemoExtendedCases(baseCwd: string): WatchCase[] { cssEntryFile: path.resolve(baseCwd, 'demo/taro-webpack-vue3-tailwindcss-v4/src/app.css'), injectMarkerElement: true, reloadAfterCssMutation: true, + compileSettleTimeoutMs: 120_000, env: { NODE_ENV: 'development', }, diff --git a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/web.ts b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/web.ts index 08bf3bb58..270663059 100644 --- a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/web.ts +++ b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/web.ts @@ -623,6 +623,7 @@ export async function runWebHmr( } let lastStyleError = '' + const reloadTimeoutMs = Math.min(options.timeoutMs, 120_000) const createReloadedStyleAcceptWhen = (expectedStyle: ReturnType) => { let lastReloadAttemptAt = 0 return async () => { @@ -634,11 +635,11 @@ export async function runWebHmr( try { await page.reload({ waitUntil: 'domcontentloaded', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) await page.locator(config.readySelector ?? 'body').waitFor({ state: 'attached', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) if (config.injectMarkerElement) { await ensureInjectedMarkerElement(page, marker) @@ -675,11 +676,11 @@ export async function runWebHmr( await waitForWebCompileSettled(hotUpdateStartedAt, 'hot-update', createReloadedStyleAcceptWhen(expectedStyle)) await page.reload({ waitUntil: 'domcontentloaded', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) await page.locator(config.readySelector ?? 'body').waitFor({ state: 'attached', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) } let computedStyle: WebHmrMetrics['computedStyle'] | undefined @@ -735,11 +736,11 @@ export async function runWebHmr( await waitForWebCompileSettled(rollbackStartedAt, 'rollback', createReloadedStyleAcceptWhen(rollbackExpectedStyle)) await page.reload({ waitUntil: 'domcontentloaded', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) await page.locator(config.readySelector ?? 'body').waitFor({ state: 'attached', - timeout: Math.min(options.timeoutMs, 60_000), + timeout: reloadTimeoutMs, }) } const rollbackEffectiveMs = await waitFor( From 684323cdbcb8f76d7e4823a64e14dd0fc1e73921 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 22:58:08 +0800 Subject: [PATCH 5/7] ci: shorten mpx watch quick gate --- .github/workflows/e2e-watch.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-watch.yml b/.github/workflows/e2e-watch.yml index 8c558b0d5..afc17361c 100644 --- a/.github/workflows/e2e-watch.yml +++ b/.github/workflows/e2e-watch.yml @@ -161,6 +161,7 @@ jobs: watch_case: mpx-tailwindcss-v4 round_profile: default watch_save_snapshots: '1' + watch_main_style_only: '1' timeout_minutes: 35 watch_timeout_ms: '240000' watch_poll_ms: '40' From 885dd56cd84c3568ac70caeea51204cdda732509 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Fri, 12 Jun 2026 23:26:13 +0800 Subject: [PATCH 6/7] ci: relax taro webpack web hmr settle window --- .../src/watch-hmr-regression/cases/demo/extended.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts index 1264a6405..d94d93503 100644 --- a/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts +++ b/tools/weapp-tailwindcss-scripts/src/watch-hmr-regression/cases/demo/extended.ts @@ -779,7 +779,7 @@ export function buildDemoExtendedCases(baseCwd: string): WatchCase[] { cssEntryFile: path.resolve(baseCwd, 'demo/taro-webpack-react-tailwindcss-v4/src/app.css'), injectMarkerElement: true, reloadAfterCssMutation: true, - compileSettleTimeoutMs: 90_000, + compileSettleTimeoutMs: 180_000, env: { NODE_ENV: 'development', }, From 115ff58f47d609a7baef95f52b38db9367e57ba8 Mon Sep 17 00:00:00 2001 From: ice breaker <1324318532@qq.com> Date: Sat, 13 Jun 2026 00:18:49 +0800 Subject: [PATCH 7/7] test: align ci assertions with postcss split --- .../test/ci/workflows.test.ts | 18 ++++++++++-------- .../test/postcss/v4-apply.test.ts | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/weapp-tailwindcss/test/ci/workflows.test.ts b/packages/weapp-tailwindcss/test/ci/workflows.test.ts index a4574cc70..e2a5e2381 100644 --- a/packages/weapp-tailwindcss/test/ci/workflows.test.ts +++ b/packages/weapp-tailwindcss/test/ci/workflows.test.ts @@ -358,7 +358,7 @@ describe('e2e watch workflow', () => { 'macos:22:mpx-tailwindcss-v4:default', 'windows:22:uni-app-vite-tailwindcss-v3:default', 'windows:22:uni-app-vite-tailwindcss-v3:issue33', - 'windows:22:taro-webpack-react-tailwindcss-v4:issue33', + 'windows:22:taro-webpack-react-tailwindcss-v4:default', 'windows:22:mpx-tailwindcss-v4:default', ])) expect(cases.some(item => item.includes(':weapp-vite-tailwindcss-'))).toBe(false) @@ -419,17 +419,19 @@ describe('e2e watch workflow', () => { }, { watch_case: 'taro-webpack-react-tailwindcss-v4', - round_profile: 'issue33', - timeout_minutes: 70, - watch_timeout_ms: '600000', - watch_command_timeout_ms: '1800000', + round_profile: 'default', + timeout_minutes: 45, + watch_timeout_ms: '420000', + watch_command_timeout_ms: '960000', + watch_web_only: '1', }, { watch_case: 'mpx-tailwindcss-v4', round_profile: 'default', - timeout_minutes: 60, - watch_timeout_ms: '600000', - watch_command_timeout_ms: '2400000', + timeout_minutes: 35, + watch_timeout_ms: '240000', + watch_command_timeout_ms: '600000', + watch_main_style_only: '1', }, ] const slowMacosUniAppPrBudgets = [ diff --git a/packages/weapp-tailwindcss/test/postcss/v4-apply.test.ts b/packages/weapp-tailwindcss/test/postcss/v4-apply.test.ts index 6d9fc7365..77ba745a0 100644 --- a/packages/weapp-tailwindcss/test/postcss/v4-apply.test.ts +++ b/packages/weapp-tailwindcss/test/postcss/v4-apply.test.ts @@ -42,7 +42,8 @@ describe('postcss v4 @apply', () => { expect(result.css).toContain('display: flex') expect(result.css).toContain('flex-direction: column') expect(result.css).toContain('align-items: center') - expect(result.css).toContain('padding-top: 32rpx') + expect(result.css).toContain('--spacing: 8rpx') + expect(result.css).toContain('padding-top: calc(var(--spacing) * 4)') expect(result.css).toContain('background-color: rgba(49, 237, 216, 0.54)') expect(result.css).not.toContain('@apply') expect(result.css).not.toContain('@reference')