Skip to content

Commit 43c1c54

Browse files
committed
feat(vite): 拆分 Generic Web CSS-only 管线
1 parent ad9c0f9 commit 43c1c54

38 files changed

Lines changed: 779 additions & 203 deletions

.changeset/web-css-only.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"weapp-tailwindcss": minor
3+
---
4+
5+
`weapp-tailwindcss/vite/web` 拆分为真正的 Generic Web CSS-only 管线:默认只处理 CSS 生成与 HMR,不注册 JS/template、框架扩展、分包和小程序收尾;主入口识别为 Generic Web 后复用相同 profile,`styleInjector` 改为显式 opt-in。

README.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
| 目标端 | 推荐入口 | 适用场景 |
4545
| --- | --- | --- |
46-
| Web / H5 | `weapp-tailwindcss/vite``/webpack``/rspack``/gulp`Node API | 浏览器 CSS、H5 和普通 Web 构建 |
46+
| Web / H5 | `weapp-tailwindcss/vite/web`(CSS-only)或 `weapp-tailwindcss/vite``/webpack``/rspack``/gulp`Node API | 浏览器 CSS、H5 和普通 Web 构建 |
4747
| 小程序 | 对应构建器入口,或 `@weapp-tailwindcss/cli --target weapp` | 微信、支付宝、抖音、QQ 等小程序 CSS |
4848
| App WebView | `weapp-tailwindcss` 的框架集成 | uni-app、Taro 等框架的 App WebView 构建 |
4949
| uni-app x | `weapp-tailwindcss/vite` | Android、iOS 与 HarmonyOS 原生应用构建 |
@@ -74,29 +74,23 @@ pnpm add -D tailwindcss weapp-tailwindcss
7474

7575
### 3. 注册构建器插件
7676

77-
以 Vite 为例,先注册框架插件,再注册 `WeappTailwindcss`
77+
以 Vite 为例,纯 Web 项目使用 CSS-only 入口
7878

7979
```ts
80-
import { dirname, resolve } from 'node:path'
81-
import { fileURLToPath } from 'node:url'
8280
import { defineConfig } from 'vite'
83-
import { WeappTailwindcss } from 'weapp-tailwindcss/vite'
84-
85-
const projectRoot = dirname(fileURLToPath(import.meta.url))
81+
import { WeappTailwindcssWeb } from 'weapp-tailwindcss/vite/web'
8682

8783
export default defineConfig({
8884
plugins: [
89-
// 先放你的框架插件,例如 uni()。
90-
WeappTailwindcss({
91-
cssEntries: [resolve(projectRoot, 'src/app.css')],
92-
cssOptions: {
93-
rem2rpx: true,
94-
},
95-
}),
85+
WeappTailwindcssWeb(),
9686
],
9787
})
9888
```
9989

90+
`vite/web` 只处理 Tailwind CSS 生成、CSS transform、CSS HMR 和 Web CSS 收尾,不注册 JS/template 转译、框架扩展、分包或小程序 finalizer。`styleInjector` 默认关闭,只有显式配置时才启用 Web 适配器。SSR、library mode、`optimizeDeps``cssMinify` 和 sourcemap 继续由 Vite 管理;CSS 入口必须实际 import 到 Vite 模块图中。
91+
92+
需要同时支持小程序、框架扩展或多入口时,继续使用 `weapp-tailwindcss/vite` 主入口,并显式指定 `generator.target``platform`。无标记的 Generic Vite 项目会在 Vite 配置解析后自动复用 Web CSS-only profile。
93+
10094
Webpack、Rspack、Gulp、Taro、uni-app、Mpx 和原生小程序的完整配置见[框架接入指南](https://tw.weapp.dev/zh-cn/docs/quick-start/frameworks/uni-app-vite)
10195

10296
## CLI

README_en.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The goal is simple: use one Tailwind input and generate the correct artifact for
4343

4444
| Target | Recommended entry | Use it for |
4545
| --- | --- | --- |
46-
| Web / H5 | `weapp-tailwindcss/vite`, `/webpack`, `/rspack`, `/gulp`, or the Node API | Browser CSS, H5, and regular Web builds |
46+
| Web / H5 | `weapp-tailwindcss/vite/web` (CSS-only), `weapp-tailwindcss/vite`, `/webpack`, `/rspack`, `/gulp`, or the Node API | Browser CSS, H5, and regular Web builds |
4747
| Mini programs | The matching builder entry, or `@weapp-tailwindcss/cli --target weapp` | WeChat, Alipay, Douyin, QQ, and other mini-program CSS |
4848
| App WebView | `weapp-tailwindcss` framework integrations | App WebView builds from frameworks such as uni-app and Taro |
4949
| uni-app x | `weapp-tailwindcss/vite` | Native Android, iOS, and HarmonyOS application builds |
@@ -74,29 +74,23 @@ The entry must be imported by the project. `cssEntries` tells the generator whic
7474

7575
### 3. Register the builder integration
7676

77-
With Vite, register the framework plugin first and `WeappTailwindcss` after it:
77+
For a pure Web project, use the CSS-only Vite entry:
7878

7979
```ts
80-
import { dirname, resolve } from 'node:path'
81-
import { fileURLToPath } from 'node:url'
8280
import { defineConfig } from 'vite'
83-
import { WeappTailwindcss } from 'weapp-tailwindcss/vite'
84-
85-
const projectRoot = dirname(fileURLToPath(import.meta.url))
81+
import { WeappTailwindcssWeb } from 'weapp-tailwindcss/vite/web'
8682

8783
export default defineConfig({
8884
plugins: [
89-
// Put your framework plugin here first, for example uni().
90-
WeappTailwindcss({
91-
cssEntries: [resolve(projectRoot, 'src/app.css')],
92-
cssOptions: {
93-
rem2rpx: true,
94-
},
95-
}),
85+
WeappTailwindcssWeb(),
9686
],
9787
})
9888
```
9989

90+
`vite/web` only handles Tailwind CSS generation, CSS transforms, CSS HMR, and Web CSS finalization. It does not register JavaScript/template transforms, framework extensions, subpackage processing, or mini-program finalizers. The built-in `styleInjector` is disabled by default and is enabled only when explicitly configured. SSR, library mode, `optimizeDeps`, `cssMinify`, and sourcemaps remain Vite responsibilities; the CSS entry must be imported into Vite's module graph.
91+
92+
When the same build also needs mini-program output, framework extensions, or multiple entries, use the main `weapp-tailwindcss/vite` entry and set `generator.target` or `platform` explicitly. An unmarked Generic Vite project automatically reuses the Web CSS-only profile after Vite resolves its configuration.
93+
10094
See the [framework integration guides](https://tw.weapp.dev/docs/quick-start/frameworks/uni-app-vite) for Webpack, Rspack, Gulp, Taro, uni-app, Mpx, and native mini-program projects.
10195

10296
## CLI

packages/weapp-tailwindcss/src/bundlers/shared/v4-generation-core.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { resolvePostcssRequestOption } from './generator-css/source-resolver/pos
1111
import { isVueScopedStyleRequest, stripRequestQuery } from './style-requests'
1212

1313
export interface TailwindV4GenerationCoreInput extends GenerateCssByGeneratorOptions {
14+
/** CSS-only 构建不需要 compiler graph/shadow artifact,固定使用 legacy 输出。 */
15+
compilerMode?: 'legacy' | undefined
1416
compilationChanges?: CompilationDependencyChange[] | undefined
1517
frameworkPostcssOwner?: InternalUserDefinedOptions | undefined
1618
cssStage?: CssStage | undefined
@@ -180,7 +182,7 @@ async function generateTailwindV4CssWithImplementation(
180182
export async function generateTailwindV4Css(
181183
options: TailwindV4GenerationCoreInput,
182184
): Promise<TailwindV4GenerationCoreResult | undefined> {
183-
const mode = resolveCompilerMode()
185+
const mode = options.compilerMode ?? resolveCompilerMode()
184186
const generationOptions = resolveGenerationOptions(options, mode)
185187
if (mode === 'legacy') {
186188
return generateTailwindV4CssWithImplementation(generationOptions, {

packages/weapp-tailwindcss/src/bundlers/vite/capability-profile.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { UserDefinedOptions } from '@/types'
44
/** Vite 插件链按运行场景拆分的能力开关。 */
55
export interface ViteCapabilityProfile {
66
name: 'framework' | 'generic-web'
7+
/** 是否只装配 CSS 生成链路,不注册 JS/template 和小程序产物处理。 */
8+
cssOnly?: boolean
79
frameworkName: ViteFrameworkName
810
generatorTarget: 'web' | 'weapp' | 'app' | undefined
911
platformFamily: 'web' | 'native-app' | 'mini-program' | 'unknown'
@@ -31,6 +33,7 @@ export const frameworkViteCapabilityProfile: ViteCapabilityProfile = {
3133
export function createGenericWebViteCapabilityProfile(options: UserDefinedOptions): ViteCapabilityProfile {
3234
return {
3335
name: 'generic-web',
36+
cssOnly: true,
3437
frameworkName: 'generic',
3538
generatorTarget: 'web',
3639
platformFamily: 'web',

packages/weapp-tailwindcss/src/bundlers/vite/css-finalizer/generic-web-fast-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { CssFinalizerContext } from './options'
44
import { stripBundlerGeneratedCssMarkers } from '../../shared/generated-css-marker'
55
import { hasTailwindGeneratedCss, hasTailwindGeneratedCssMarkers } from '../../shared/generator-css'
66
import { normalizeOutputPathKey } from '../../shared/module-graph'
7-
import { resolveViteCssPipelineOutputFile } from '../generate-bundle'
7+
import { resolveViteCssPipelineOutputFile } from '../css-output'
88
import { collectMatchingGeneratedCssMarkerFiles, resolveViteProcessedCssAssetSource } from '../processed-css-assets/markers-imports'
99
import { shouldUseGenericWebFinalizerFastPath } from '../shared/generic-web-production-fast-path'
1010
import { finalizeWebCss } from './options'
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { OutputBundle } from 'rollup'
2+
import type { Plugin } from 'vite'
3+
import type { CssFinalizerContext } from './options'
4+
import path from 'node:path'
5+
import process from 'node:process'
6+
import { disposeCompilerOwner, finalizeCompilerShadowRun } from '@/compiler'
7+
import { tryFinalizeGenericWebCss } from './generic-web-fast-path'
8+
9+
/**
10+
* Generic Web 专用 CSS finalizer。
11+
*
12+
* 该插件只处理已经在 CSS transform 阶段记录的 CSS asset,不扫描或改写
13+
* HTML、JavaScript、模板和小程序分包产物。
14+
*/
15+
export function createViteWebCssFinalizerOutputPlugin(context: CssFinalizerContext): Plugin {
16+
return {
17+
name: 'weapp-tailwindcss:adaptor:web-css-finalizer',
18+
enforce: 'post',
19+
async closeBundle() {
20+
if (context.getResolvedConfig()?.build?.watch != null) {
21+
return
22+
}
23+
await disposeCompilerOwner(context.runtimeState)
24+
},
25+
async closeWatcher() {
26+
await disposeCompilerOwner(context.runtimeState)
27+
},
28+
generateBundle: {
29+
order: 'post',
30+
async handler(_options, bundle: OutputBundle) {
31+
const resolvedConfig = context.getResolvedConfig()
32+
if (resolvedConfig?.command !== 'build') {
33+
return
34+
}
35+
const rootDir = resolvedConfig.root ? path.resolve(resolvedConfig.root) : process.cwd()
36+
const recordTiming = (phase: string, startedAt: number) => {
37+
context.hmrTimingRecorder?.record(`webCssFinalizer.${phase}`, performance.now() - startedAt)
38+
}
39+
const startedAt = performance.now()
40+
const finalized = tryFinalizeGenericWebCss({
41+
bundle,
42+
context,
43+
createCssPipelineContext: file => ({
44+
bundle,
45+
currentGeneratorBranch: {
46+
isWeb: true,
47+
isNativeApp: false,
48+
isMiniProgram: false,
49+
},
50+
currentGeneratorOptions: {
51+
target: 'web',
52+
enabled: true,
53+
},
54+
file,
55+
opts: context.opts,
56+
resolvedConfig,
57+
resolveStylePlatform: () => 'web',
58+
} as any),
59+
isHarmonyAppStyleTarget: false,
60+
isNativeAppStyleTarget: false,
61+
isWebGeneratorTarget: true,
62+
recordTiming,
63+
rootDir,
64+
sourceRoot: undefined,
65+
})
66+
if (!finalized) {
67+
context.debug('Generic Web CSS finalizer skipped: no unique processed CSS asset')
68+
}
69+
finalizeCompilerShadowRun(context.runtimeState)
70+
context.hmrTimingRecorder?.record('webCssFinalizer.total', performance.now() - startedAt)
71+
},
72+
},
73+
}
74+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { InternalUserDefinedOptions, IStyleHandlerOptions } from '@/types'
2+
import path from 'node:path'
3+
import { normalizeStyleHandlerMajorVersion } from '@/context/style-options'
4+
import { normalizeOutputPathKey } from '../shared/module-graph'
5+
import { stripRequestQuery } from '../shared/style-requests'
6+
7+
type CssHandlerOptions = IStyleHandlerOptions & {
8+
postcssOptions: {
9+
options: {
10+
from: string
11+
}
12+
}
13+
majorVersion?: 4 | undefined
14+
sourceOptions?: {
15+
outputRoot?: string | undefined
16+
requestFile?: string | undefined
17+
} | undefined
18+
}
19+
20+
interface CssHandlerOptionsCacheOptions {
21+
getAppType: () => InternalUserDefinedOptions['appType']
22+
mainCssChunkMatcher: InternalUserDefinedOptions['mainCssChunkMatcher']
23+
getMajorVersion: () => number | undefined
24+
getOutputRoot?: (() => string | undefined) | undefined
25+
getExtraOptions?: ((file: string) => Partial<IStyleHandlerOptions>) | undefined
26+
getDynamicCssOptions?: (() => Partial<IStyleHandlerOptions>) | undefined
27+
}
28+
29+
export interface CssHandlerOptionsCache {
30+
getCssHandlerOptions: (file: string) => CssHandlerOptions
31+
getCssUserHandlerOptions: (file: string) => CssHandlerOptions
32+
}
33+
34+
export function resolveViteCssHandlerExtraOptions(file: string) {
35+
const normalized = normalizeOutputPathKey(file.replace(/[?#].*$/, ''))
36+
return normalized.includes('/')
37+
? { isMainChunk: false }
38+
: {}
39+
}
40+
41+
export function createCssHandlerOptionsCache(options: CssHandlerOptionsCacheOptions): CssHandlerOptionsCache {
42+
const cssHandlerOptionsCache = new Map<string, CssHandlerOptions>()
43+
const cssUserHandlerOptionsCache = new Map<string, CssHandlerOptions>()
44+
45+
const getCssHandlerOptions = (file: string) => {
46+
const majorVersion = options.getMajorVersion()
47+
const appType = options.getAppType()
48+
const isMainChunk = options.mainCssChunkMatcher(file, appType)
49+
const outputRoot = options.getOutputRoot?.()
50+
const sourceFile = stripRequestQuery(file)
51+
const from = path.isAbsolute(sourceFile)
52+
? sourceFile
53+
: outputRoot
54+
? path.resolve(outputRoot, sourceFile)
55+
: sourceFile
56+
const extraOptions = options.getExtraOptions?.(file) ?? {}
57+
const dynamicCssOptions = options.getDynamicCssOptions?.() ?? {}
58+
const cacheKey = `${majorVersion ?? 'unknown'}:${appType ?? 'unknown'}:${isMainChunk ? '1' : '0'}:${outputRoot ?? ''}:${file}:${JSON.stringify(extraOptions)}:${JSON.stringify(dynamicCssOptions)}`
59+
const cached = cssHandlerOptionsCache.get(cacheKey)
60+
if (cached) {
61+
return cached
62+
}
63+
64+
const created = {
65+
isMainChunk,
66+
...extraOptions,
67+
...dynamicCssOptions,
68+
postcssOptions: {
69+
options: {
70+
from,
71+
},
72+
},
73+
majorVersion: normalizeStyleHandlerMajorVersion(majorVersion),
74+
sourceOptions: {
75+
outputRoot,
76+
...(sourceFile === file ? {} : { requestFile: file }),
77+
},
78+
}
79+
cssHandlerOptionsCache.set(cacheKey, created)
80+
return created
81+
}
82+
83+
const getCssUserHandlerOptions = (file: string) => {
84+
const majorVersion = options.getMajorVersion()
85+
const outputRoot = options.getOutputRoot?.()
86+
const cacheKey = `${majorVersion ?? 'unknown'}:${outputRoot ?? ''}:${file}`
87+
const cached = cssUserHandlerOptionsCache.get(cacheKey)
88+
if (cached) {
89+
return cached
90+
}
91+
92+
const created = {
93+
...getCssHandlerOptions(file),
94+
isMainChunk: false,
95+
}
96+
cssUserHandlerOptionsCache.set(cacheKey, created)
97+
return created
98+
}
99+
100+
return {
101+
getCssHandlerOptions,
102+
getCssUserHandlerOptions,
103+
}
104+
}

packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle/css-output.ts renamed to packages/weapp-tailwindcss/src/bundlers/vite/css-output.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { InternalUserDefinedOptions } from '@/types'
22
import { existsSync, realpathSync } from 'node:fs'
33
import path from 'node:path'
44
import { postcss } from '@weapp-tailwindcss/postcss'
5-
import { normalizeOutputPathKey } from '../../shared/module-graph'
6-
import { isCSSRequest } from '../utils'
5+
import { normalizeOutputPathKey } from '../shared/module-graph'
6+
import { isCSSRequest } from './utils'
77

88
export const SOURCE_STYLE_OUTPUT_EXT_RE = /\.(?:less|sass|scss|styl|stylus|pcss|postcss)$/i
99
export const CSS_SOURCE_OUTPUT_EXT_RE = /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss)$/i
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { InternalUserDefinedOptions, UserDefinedOptions } from '@/types'
2+
import { createBuiltinViteWebStyleInjectorPlugins } from '@/style-injector/vite-web'
3+
import { createGenericWebViteCapabilityProfile } from '../../capability-profile'
4+
import { createCssOnlyVitePlugins } from '../../shared/create-css-only-plugins-runtime'
5+
import { createViteSourceOutputRelationOwner, withViteSourceOutputRelationOwner } from '../../source-output-relations'
6+
7+
/** Generic Web CSS-only profile,供 `vite/web` 入口直接使用。 */
8+
export function createGenericWebVitePlugins(options: UserDefinedOptions | InternalUserDefinedOptions = {}) {
9+
const capability = createGenericWebViteCapabilityProfile(options as UserDefinedOptions)
10+
const owner = createViteSourceOutputRelationOwner()
11+
const plugins = withViteSourceOutputRelationOwner(owner, () => createCssOnlyVitePlugins({
12+
...options,
13+
__internalViteCapabilityProfile: capability,
14+
__internalViteWebStyleInjectorFactory: createBuiltinViteWebStyleInjectorPlugins,
15+
} as InternalUserDefinedOptions))
16+
if (!plugins) {
17+
owner.dispose()
18+
return undefined
19+
}
20+
return plugins.map((plugin) => {
21+
const wrapped: any = { ...plugin }
22+
if (plugin.watchChange) {
23+
const hook = plugin.watchChange
24+
wrapped.watchChange = async function (this: unknown, ...args: any[]) {
25+
const id = args[0]
26+
const change = args[1]
27+
if (change?.event === 'delete') {
28+
owner.removeSource(id)
29+
}
30+
else if (typeof id === 'string') {
31+
owner.observeSource(id)
32+
}
33+
return typeof hook === 'function' ? hook.apply(this, args) : hook.handler?.apply(this, args)
34+
}
35+
}
36+
if (plugin.handleHotUpdate) {
37+
const hook = plugin.handleHotUpdate
38+
wrapped.handleHotUpdate = async function (this: unknown, ...args: any[]) {
39+
const file = args[0]?.file
40+
if (typeof file === 'string') {
41+
owner.observeSource(file)
42+
}
43+
return typeof hook === 'function' ? hook.apply(this, args) : hook.handler?.apply(this, args)
44+
}
45+
}
46+
if (plugin.closeBundle) {
47+
const hook = plugin.closeBundle
48+
wrapped.closeBundle = async function (this: unknown, ...args: any[]) {
49+
try {
50+
return typeof hook === 'function' ? await hook.apply(this, args) : await hook.handler?.apply(this, args)
51+
}
52+
finally {
53+
owner.dispose()
54+
}
55+
}
56+
}
57+
return wrapped
58+
})
59+
}

0 commit comments

Comments
 (0)