weapp-tailwindcss
Version:
把 tailwindcss 原子化样式思想,带给小程序开发者们! bring tailwindcss to miniprogram developers!
547 lines (525 loc) • 24.2 kB
TypeScript
import { ParserOptions, ParseError } from '@babel/parser';
import { IMangleOptions, IMangleScopeContext } from '@weapp-tailwindcss/mangle';
export { IMangleScopeContext } from '@weapp-tailwindcss/mangle';
import { CssPreflightOptions, LoadedPostcssOptions, CustomRuleCallback, IStyleHandlerOptions } from '@weapp-tailwindcss/postcss';
export { CssPreflightOptions, IStyleHandlerOptions } from '@weapp-tailwindcss/postcss';
import { SourceMap } from 'magic-string';
import { Result, Root, Document } from 'postcss';
import { PatchOptions, ILengthUnitsPatchOptions, TailwindcssPatcherOptions, TailwindcssPatcher } from 'tailwindcss-patch';
import { Buffer } from 'node:buffer';
import { sources } from 'webpack';
import { LRUCache } from 'lru-cache';
import { pluginOptions } from 'postcss-preset-env';
interface HashMapValue {
hash: string;
changed: boolean;
}
type HashMapKey = string | number;
type CacheValue = sources.Source | string;
interface ICreateCacheReturnType {
hashMap: Map<HashMapKey, HashMapValue>;
instance: LRUCache<string, CacheValue>;
hasHashKey: (key: HashMapKey) => boolean;
getHashValue: (key: HashMapKey) => HashMapValue | undefined;
setHashValue: (key: HashMapKey, value: HashMapValue) => this['hashMap'];
computeHash: (message: string | Buffer) => string;
get: <V extends CacheValue = sources.Source>(key: string) => V | undefined;
set: <V extends CacheValue = sources.Source>(key: string, value: V) => this['instance'];
has: (key: string) => boolean;
calcHashValueChanged: (key: HashMapKey, hash: string) => this;
process: (key: string, callback: () => void | false | Promise<void | false>, fallback: () => void | {
key: string;
source: CacheValue;
} | Promise<void | {
key: string;
source: CacheValue;
}>) => void | Promise<void>;
}
interface UserDefinedOptions {
/**
* @group 0.重要配置
* @description 是否禁用此插件,一般用于构建到多平台时使用,比如小程序时不传,非小程序环境(h5,app)传入一个 `true`
* ```ts
* // 比如 uni-app vue3 vite
* import process from 'node:process'
const isH5 = process.env.UNI_PLATFORM === 'h5'
const isApp = process.env.UNI_PLATFORM === 'app'
const WeappTailwindcssDisabled = isH5 || isApp
import { UnifiedViteWeappTailwindcssPlugin as uvtw } from 'weapp-tailwindcss/vite'
// 注册插件
// highlight-start
uvtw({
disabled: WeappTailwindcssDisabled,
}),
// highlight-end
* ```
*/
disabled?: boolean;
/**
* @group 0.重要配置
* @description **这是一个重要的配置!**
它可以自定义`wxml`标签上的`attr`转化属性。默认转化所有的`class`和`hover-class`,这个`Map`的 `key`为匹配标签,`value`为属性字符串或者匹配正则数组。
如果你想要增加,对于所有标签都生效的转化的属性,你可以添加 `*`: `(string | Regexp)[]` 这样的键值对。(`*` 是一个特殊值,代表所有标签)
更复杂的情况,可以传一个 `Map<string | Regex, (string | Regex)[]>`实例。
假如你要把 `className` 通过组件的`prop`传递给子组件,又或者想要自定义转化的标签属性时,需要用到此配置,案例详见:[issue#129](https://github.com/sonofmagic/weapp-tailwindcss/issues/129#issuecomment-1340914688),[issue#134](https://github.com/sonofmagic/weapp-tailwindcss/issues/134#issuecomment-1351288238)
@example
```js
const customAttributes = {
// 匹配所有带 Class / class 相关的标签,比如某个组件上的 `a-class`, `testClass` , `custom-class` 里面的值
'*': [ /[A-Za-z]?[A-Za-z-]*[Cc]lass/ ],
// 额外匹配转化 `van-image` 标签上属性为 `custom-class` 的值
'van-image': ['custom-class'],
// 转化所有 `ice-button` 标签上属性为 `testClass` 的值
'ice-button': ['testClass']
}
```
当然你可以根据自己的需求,定义单个或者多个正则/字符串。
甚至有可能你编写正则表达式,它们匹配的范围,直接包括了插件里自带默认的 `class`/`hover-class`,那么这种情况下,你完全可以取代插件的默认模板转化器,开启 [disabledDefaultTemplateHandler](/docs/api/interfaces/UserDefinedOptions#disableddefaulttemplatehandler) 配置项,禁用默认的模版匹配转化器。
*/
customAttributes?: ICustomAttributes;
/**
* @group 0.重要配置
* @description 自定义转化class名称字典,这个配置项用来自定义转化`class`名称字典,你可以使用这个选项来简化生成的`class`
- 默认模式: 把小程序中不允许的字符串,转义为**相等长度**的代替字符串,这种情况不追求转化目标字符串的一比一绝对等价,即无法从生成结果,反推原先的`class`
当然,你也可以自定义,传一个 `Record<string, string>` 类型,只需保证转化后 css 中的 `class` 选择器,不会和自己定义的 `class` 产生冲突即可,示例见[dic.ts](https://github.com/sonofmagic/weapp-core/blob/main/packages/escape/src/dic.ts)
* @default MappingChars2String
*/
customReplaceDictionary?: Record<string, string>;
/**
* @version `^4.0.0`
* @group 0.重要配置
* @description js 忽略标签模板表达式中的标识符,这样使用标识符包裹的模板字符串不会被转义
* @default ['weappTwIgnore']
*/
ignoreTaggedTemplateExpressionIdentifiers?: (string | RegExp)[];
/**
* @version `^4.0.0`
* @group 0.重要配置
* @description js 忽略调用表达式中的标识符,这样使用这个方法,包裹的模板字符串和字符串字面量不会被转义,一般用来配合 `@weapp-tailwindcss/merge` 使用,比如设置为 `['twMerge', 'twJoin', 'cva']`
*/
ignoreCallExpressionIdentifiers?: (string | RegExp)[];
/**
* @group 0.重要配置
* @issue https://github.com/sonofmagic/weapp-tailwindcss/issues/7
* @description 在所有 view节点添加的 css 预设,可根据情况自由的禁用原先的规则,或者添加新的规则。默认预置 `css` 同 `tailwindcss` 类似,详细用法如下:
* ```js
* // default 默认,这代表会添加给所有的 view / text 元素, 受到 cssPreflightRange 配置项影响 :
cssPreflight: {
'box-sizing': 'border-box',
'border-width': '0',
'border-style': 'solid',
'border-color': 'currentColor'
}
// result
// box-sizing: border-box;
// border-width: 0;
// border-style: solid;
// border-color: currentColor
// case 禁用所有
cssPreflight: false
// result
// none
// case 禁用单个属性
cssPreflight: {
'box-sizing': false
}
// border-width: 0;
// border-style: solid;
// border-color: currentColor
// case 更改和添加单个属性
cssPreflight: {
'box-sizing': 'content-box',
'background': 'black'
}
// result
// box-sizing: content-box;
// border-width: 0;
// border-style: solid;
// border-color: currentColor;
// background: black
* ```
*/
cssPreflight?: CssPreflightOptions;
/**
* @group 0.重要配置
* @issue https://github.com/sonofmagic/weapp-tailwindcss/pull/62
* @description 全局`dom`选择器,只有在这个选择器作用范围内的`dom`会被注入 `cssPreflight` 的变量和默认样式。只对所有的 `view`,`text` 和伪元素生效,想要对所有的元素生效,可切换为 `'all'`,此时需要自行处理和客户端默认样式的冲突
*/
cssPreflightRange?: 'all';
/**
* @group 0.重要配置
* @version `^2.6.0`
* @description 是否注入额外的 `tailwindcss css var scope` 区域,这个选项用于这样的场景
*
* 比如 `taro vue3` 使用 [NutUI](https://nutui.jd.com), 需要使用 `@tarojs/plugin-html`,而这个插件会启用 `postcss-html-transform` 从而移除所有带 `*` 选择器
*
* 这会导致 `tailwindcss css var scope` 区域被移除导致一些样式,比如渐变等等功能失效
*
* 这种场景下,启用这个选项会再次重新注入整个 `tailwindcss css var scope`
*
* @default false
*/
injectAdditionalCssVarScope?: boolean;
/**
* @group 0.重要配置
* @description 用于处理 css 选择器的替换规则
*/
cssSelectorReplacement?: {
/**
* @default `'page'` <br/>
* @description 把`css`中的全局选择器 **`:root`** 替换为指定值,默认替换为 `'page'`,设置为 `false` 时不进行替换
*/
root?: string | string[] | false;
/**
* @issue https://github.com/sonofmagic/weapp-tailwindcss/issues/81 <br/>
* @default `['view','text']` <br/>
* @description 把`css`中的全局选择器 **`*`** 替换为指定值,默认替换为 `'view','text'`,设置为 `false` 时不进行替换,此时小程序会由于不认识`*`选择器而报错
*/
universal?: string | string[] | false;
};
/**
* @version `^3.0.0`
* @group 0.重要配置
* @description rem 转 rpx 配置,默认为 `undefined` 不开启,可传入 `true` 启用默认配置项,也可传入自定义配置项,配置项列表见 [postcss-rem-to-responsive-pixel](https://www.npmjs.com/package/postcss-rem-to-responsive-pixel)
* ```ts
* // 默认值
* {
* rootValue: 32,
* propList: ['*'],
* transformUnit: 'rpx',
* }
* ```
*/
rem2rpx?: boolean | UserDefinedOptions;
/**
* @version `^4.0.0`
* @group 0.重要配置
* @description postcss-preset-env 的入参数
*/
cssPresetEnv?: pluginOptions;
/**
* @version `^4.0.0`
* @group 0.重要配置
* @description 配置不同版本 tailwindcss 的行为
*/
tailwindcss?: PatchOptions['tailwindcss'];
}
interface UserDefinedOptions {
/**
* @group 1.文件匹配
* @description 匹配 `wxml`等等模板进行处理的方法
*/
htmlMatcher?: (name: string) => boolean;
/**
* @group 1.文件匹配
* @description 匹配 `wxss` 等等样式文件的方法
*/
cssMatcher?: (name: string) => boolean;
/**
* @group 1.文件匹配
* @description 匹配编译后 `js` 文件进行处理的方法
*/
jsMatcher?: (name: string) => boolean;
/**
* @group 1.文件匹配
* @description `tailwindcss css var inject scope` 的匹配方法,用于处理原始变量和替换不兼容选择器。可以不传,但是遇到某些 `::before/::after` 选择器注入冲突时,建议传入参数手动指定 css bundle 文件位置
*
*/
mainCssChunkMatcher?: (name: string, appType?: AppType) => boolean;
/**
* @group 1.文件匹配
* @experiment 实验性质,有可能会改变
* @description 各个平台 `wxs` 文件的匹配方法,可以设置为包括微信的 .wxs,支付宝的 .sjs 和 百度小程序的 .filter.js
* > tip: 记得在 `tailwind.config.js` 中,把 `wxs` 这个格式加入 `content` 配置项,不然不会生效
* @default ()=>false
*/
wxsMatcher?: (name: string) => boolean;
/**
* @group 1.文件匹配
* @experiment 实验性质,有可能会改变
* @description 是否转义 `wxml` 中内联的 `wxs`
* > tip: 记得在 `tailwind.config.js` 中,把 `wxs` 这个格式加入 `content` 配置项,不然不会生效
* @example
* ```html
* <!-- index.wxml -->
* <wxs module="inline">
// 我是内联wxs
// 下方的类名会被转义
var className = "after:content-['我是className']"
module.exports = {
className: className
}
</wxs>
<wxs src="./index.wxs" module="outside"/>
<view><view class="{{inline.className}}"></view><view class="{{outside.className}}"></view></view>
* ```
* @default false
*/
inlineWxs?: boolean;
}
interface UserDefinedOptions {
/**
* @group 2.生命周期
* @description plugin apply 初调用
*/
onLoad?: () => void;
/**
* @group 2.生命周期
* @description 开始处理时调用
*/
onStart?: () => void;
/**
* @description 匹配成功并修改文件内容前调用
*/
/**
* @group 2.生命周期
* @description 匹配成功并修改文件内容后调用
*/
onUpdate?: (filename: string, oldVal: string, newVal: string) => void;
/**
* @group 2.生命周期
* @description 结束处理时调用
*/
onEnd?: () => void;
}
interface UserDefinedOptions {
/**
* @group 3.一般配置
* @issue https://github.com/sonofmagic/weapp-tailwindcss/issues/110
* @description 自从`tailwindcss 3.2.0`对任意值添加了长度单位的校验后,小程序中的`rpx`这个`wxss`单位,由于不在长度合法名单中,于是被识别成了颜色,导致与预期不符,详见:[issues/110](https://github.com/sonofmagic/weapp-tailwindcss/issues/110)。所以这个选项是用来给`tailwindcss`运行时,自动打上一个支持`rpx`单位的补丁。默认开启,在绝大部分情况下,你都可以忽略这个配置项,除非你需要更高级的自定义。
> 目前自动检索可能存在一定的缺陷,它会在第一次运行的时候不生效,关闭后第二次运行才生效。这是因为 nodejs 运行时先加载好了 `tailwindcss` 模块 ,然后再来运行这个插件,自动给 `tailwindcss` 运行时打上 `patch`。此时由于 `tailwindcss` 模块已经加载,所以 `patch` 在第一次运行时不生效,`ctrl+c` 关闭之后,再次运行才生效。这种情况可以使用:
```diff
"scripts": {
+ "postinstall": "weapp-tw patch"
}
```
使用 `npm hooks` 的方式来给 `tailwindcss` 自动打 `patch`
*/
supportCustomLengthUnitsPatch?: ILengthUnitsPatchOptions | boolean;
/**
* @group 3.一般配置
* @description 使用的框架类型(uni-app,taro...),用于找到主要的 `css bundle` 进行转化,这个配置会影响默认方法 `mainCssChunkMatcher` 的行为,不传会去猜测 `tailwindcss css var inject scope` (tailwindcss 变量注入的位置) 的位置
*/
appType?: AppType;
/**
* @group 3.一般配置
* @description 针对 tailwindcss arbitrary values 的一些配置
*/
arbitraryValues?: IArbitraryValues;
/**
* @group 3.一般配置
* @version `^2.6.1`
* @description 当 `tailwindcss` 和 `js` 处理的字面量撞车的时候,配置此选项可以用来保留js字面量,不进行转义处理。返回值中,想要当前js字面量保留,则返回 `true`。想要转义则返回 `false/undefined`
* @default 保留所有带 `*` js字符串字面量
*/
jsPreserveClass?: (keyword: string) => boolean | undefined;
/**
* @group 3.一般配置
* @version `^2.6.2`
* @description 开启此选项,将会禁用默认 `wxml` 模板替换器,此时模板的匹配和转化将完全被 [`customAttributes`](/docs/api/interfaces/UserDefinedOptions#customattributes) 接管,
*
* 此时你需要自己编写匹配之前默认 `class`/`hover-class`,以及新的标签属性的正则表达式`regex`
* @default false
*/
disabledDefaultTemplateHandler?: boolean;
/**
* @ignore
* @internal
*/
runtimeLoaderPath?: string;
/**
* @group 3.一般配置
* @version `^2.9.3`
* @description 用于指定路径来获取 tailwindcss 上下文,一般情况下不用传入,使用 linked / monorepo 可能需要指定具体位置,路径通常是目标项目的 package.json 所在目录
*/
tailwindcssBasedir?: string;
/**
* @group 3.一般配置
* @version `^3.0.11`
* @description 缓存策略
*/
cache?: boolean | ICreateCacheReturnType;
/**
* @version `^3.2.0`
* @group 3.一般配置
* @description 对解析 js 使用的 `@babel/parser` 工具的配置
*/
babelParserOptions?: ParserOptions & {
cache?: boolean;
};
/**
* @group 3.一般配置
* @description 用于控制 tailwindcss 子组合器的生效标签范围, 这里我们用一个例子来说明这个配置是干啥用的.
*
* 我们布局的时候往往会使用 `space-x-4`
* 那么实际上会生成这样的css选择器:
* ```css
* .space-x-4>:not([hidden])~:not([hidden]){}
* ```
* 然而很不幸,这个选择器在小程序中是不支持的,写了会报错导致编译失败。
* 所以出于保守起见,我把它替换为了:
* ```css
* .space-x-4>view + view{}
* ```
* 这同时也是默认值, 而这个选项就允许你进行自定义子组合器的行为
*
* 你可以传入一个 字符串,或者字符串数组
* 1. 传入字符串数组,比如 `['view','text']` 生成:
* ```css
* .space-y-4>view + view,text + text{}
* ```
*
* 2. 传入一个字符串,此时行为变成了整个替换,比如 `'view,text,button,input ~ view,text,button,input'` 生成:
* ```css
* .space-y-4>view,text,button,input ~ view,text,button,input{}
* ```
* @default 'view + view'
*/
cssChildCombinatorReplaceValue?: string | string[];
/**
* @version `^3.2.0`
* @group 3.一般配置
* @description 对解析 css 使用的 `postcss` 工具的配置
*/
postcssOptions?: LoadedPostcssOptions;
/**
* @version `^3.2.1`
* @group 3.一般配置
* @issue https://github.com/sonofmagic/weapp-tailwindcss/issues/293
* @default `true`
* @description 是否删除 css :hover 选择器节点,默认为 `true`, 原因在于,小程序 css :hover 是不生效的,要使用 view 这种标签的 hover-class 属性
*/
cssRemoveHoverPseudoClass?: boolean;
/**
* @version `^4.1.2`
* @group 3.一般配置
* @default `true`
* @description 是否删除 `@property` 选择器节点,默认为 `true`, 原因在于,小程序 `@property` 是不生效的
*/
cssRemoveProperty?: boolean;
/**
* @group 3.一般配置
* @description 用于自定义处理 css 的回调函数,可根据 Postcss walk 方法自由定制处理方案的 callback 方法
*/
customRuleCallback?: CustomRuleCallback;
/**
* @group 3.一般配置
* @description 自定义 patcher 参数
*/
tailwindcssPatcherOptions?: TailwindcssPatcherOptions;
}
interface UserDefinedOptions {
/**
* @group 4.即将废弃配置
* @description 对解析 js 使用的 ast 工具,默认情况使用 `babel`,可以通过安装 `@ast-grep/napi`,同时启用 `ast-grep` 配置项,来启用 `ast-grep` 来处理 `js`,速度会是 `babel` 的 `2` 倍左右
* :::danger
* 此配置即将在 `5.x` 被弃用
*
* 废弃原因:
*
* 虽然 `@ast-grep/napi` 提供了更快的速度,但是 `babel` 有更强的静态分析 `js` 能力,使得后续一些新功能的开发无法使用 `@ast-grep/napi` 实现,所以废弃只保留 `babel` 的方式
* :::
*/
jsAstTool?: 'babel' | 'ast-grep';
/**
* @group 4.即将废弃配置
* @description 是否压缩混淆 `wxml`,`js` 和 `wxss` 中指定范围的 `class` 以避免选择器过长问题,默认为`false`不开启,详细配置见 [unplugin-tailwindcss-mangle](https://github.com/sonofmagic/tailwindcss-mangle/tree/main/packages/unplugin-tailwindcss-mangle)
* :::danger
* 此配置即将在 `5.x` 被弃用
*
* 废弃原因:
*
* mangle 相关的功能会被迁移到另外一个项目: [`tailwindcss-mangle`](https://github.com/sonofmagic/tailwindcss-mangle) 中去,还想要这个功能可以2个插件结合使用
* :::
*/
mangle?: boolean | IMangleOptions;
}
type ItemOrItemArray<T> = T | T[];
type AppType = 'uni-app' | 'uni-app-vite' | 'taro' | 'remax' | 'rax' | 'native' | 'kbone' | 'mpx' | 'weapp-vite';
interface InternalCssSelectorReplacerOptions {
mangleContext?: IMangleScopeContext;
escapeMap?: Record<string, string>;
}
interface JsHandlerResult {
code: string;
map?: SourceMap;
error?: ParseError;
}
type ICustomAttributes = Record<string, ItemOrItemArray<string | RegExp>> | Map<string | RegExp, ItemOrItemArray<string | RegExp>>;
type ICustomAttributesEntities = [string | RegExp, ItemOrItemArray<string | RegExp>][];
interface IJsHandlerOptions {
escapeMap?: Record<string, string>;
classNameSet?: Set<string>;
arbitraryValues?: IArbitraryValues;
mangleContext?: IMangleScopeContext;
jsPreserveClass?: (keyword: string) => boolean | undefined;
needEscaped?: boolean;
generateMap?: boolean;
alwaysEscape?: boolean;
unescapeUnicode?: boolean;
babelParserOptions?: ParserOptions;
ignoreTaggedTemplateExpressionIdentifiers?: (string | RegExp)[];
ignoreCallExpressionIdentifiers?: (string | RegExp)[];
}
interface IArbitraryValues {
/**
* 是否允许在类名里,使用双引号。
* 建议不要开启,因为有些框架,比如 `vue3` 它针对有些静态模板会直接编译成 `html` 字符串,此时开启这个配置很有可能导致转义出错
*
* @example
* ```html
* <!-- 开启前默认只允许单引号 -->
* <view class="after:content-['对酒当歌,人生几何']"></view>
* <!-- 开启后 -->
* <view class="after:content-[\"对酒当歌,人生几何\"]"></view>
* ```
*
* @default `false`
*/
allowDoubleQuotes?: boolean;
}
interface JsHandler {
(rawSource: string, set: Set<string>, options?: CreateJsHandlerOptions): JsHandlerResult;
}
interface ICommonReplaceOptions {
keepEOL?: boolean;
escapeMap?: Record<string, string>;
}
interface ITemplateHandlerOptions extends ICommonReplaceOptions {
customAttributesEntities?: ICustomAttributesEntities;
escapeMap?: Record<string, string>;
mangleContext?: IMangleScopeContext;
inlineWxs?: boolean;
jsHandler?: JsHandler;
runtimeSet?: Set<string>;
disabledDefaultTemplateHandler?: boolean;
quote?: string | null;
ignoreHead?: boolean;
}
type InternalUserDefinedOptions = Required<Omit<UserDefinedOptions, 'supportCustomLengthUnitsPatch' | 'customReplaceDictionary' | 'cache'> & {
supportCustomLengthUnitsPatch: ILengthUnitsPatchOptions | boolean;
templateHandler: (rawSource: string, options?: ITemplateHandlerOptions) => Promise<string>;
styleHandler: (rawSource: string, options?: IStyleHandlerOptions) => Promise<Result<Root | Document>>;
jsHandler: JsHandler;
escapeMap: Record<string, string>;
customReplaceDictionary: Record<string, string>;
setMangleRuntimeSet: (runtimeSet: Set<string>) => void;
cache: ICreateCacheReturnType;
twPatcher: TailwindcssPatcher;
}>;
type InternalPostcssOptions = Pick<UserDefinedOptions, 'cssMatcher' | 'mainCssChunkMatcher' | 'cssPreflight' | 'cssPreflightRange' | 'customRuleCallback' | 'disabled'>;
interface IBaseWebpackPlugin {
options: InternalUserDefinedOptions;
appType?: AppType;
apply: (compiler: any) => void;
}
/**
* @description InternalPatchResult
*/
interface InternalPatchResult {
dataTypes?: string;
processTailwindFeatures?: string;
plugin?: string;
}
type CreateJsHandlerOptions = Omit<IJsHandlerOptions, 'classNameSet'>;
export type { AppType, CreateJsHandlerOptions, IArbitraryValues, IBaseWebpackPlugin, ICommonReplaceOptions, ICustomAttributes, ICustomAttributesEntities, IJsHandlerOptions, ITemplateHandlerOptions, InternalCssSelectorReplacerOptions, InternalPatchResult, InternalPostcssOptions, InternalUserDefinedOptions, ItemOrItemArray, JsHandler, JsHandlerResult, UserDefinedOptions };