├── .nvmrc ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── .github ├── FUNDING.yml └── workflows │ ├── pkg.pr.new.yml │ ├── release.yml │ └── ci.yml ├── packages └── core │ ├── src │ ├── tools │ │ ├── index.ts │ │ ├── postcss.ts │ │ ├── babel.ts │ │ └── babel.test.ts │ ├── style │ │ ├── index.ts │ │ └── index.test.ts │ ├── script │ │ └── index.ts │ ├── template │ │ ├── index.ts │ │ └── index.test.ts │ ├── index.ts │ ├── utils.ts │ └── options.ts │ ├── build.config.ts │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── pnpm-workspace.yaml ├── playground ├── src │ ├── static │ │ └── logo.png │ ├── styles │ │ ├── preflight.css │ │ └── tailwind.css │ ├── main.ts │ ├── App.vue │ ├── pages.json │ ├── pages │ │ └── index │ │ │ └── index.vue │ └── manifest.json ├── tsconfig.json ├── index.html ├── tailwind.config.ts ├── vite.config.ts └── package.json ├── lint-staged.config.mjs ├── simple-git-hooks.cjs ├── commitlint.config.mjs ├── vitest.config.ts ├── .editorconfig ├── tsconfig.json ├── lerna.json ├── biome.json ├── LICENSE ├── deprecated.md ├── package.json ├── .gitignore ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ModyQyW] 2 | custom: ["https://github.com/ModyQyW/sponsors"] 3 | -------------------------------------------------------------------------------- /packages/core/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./babel"; 2 | export * from "./postcss"; 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - playground 4 | - packages/* 5 | - examples/* 6 | -------------------------------------------------------------------------------- /playground/src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uni-helper/vite-plugin-uni-tailwind/HEAD/playground/src/static/logo.png -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "*": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", 3 | }; 4 | -------------------------------------------------------------------------------- /simple-git-hooks.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "commit-msg": "npx commitlint --edit ${1}", 3 | "pre-commit": "npx lint-staged", 4 | }; 5 | -------------------------------------------------------------------------------- /commitlint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: [ 3 | "@commitlint/config-conventional", 4 | "@commitlint/config-pnpm-scopes", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | clearMocks: true, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /playground/src/styles/preflight.css: -------------------------------------------------------------------------------- 1 | button, 2 | button::after { 3 | all: unset; 4 | } 5 | 6 | button { 7 | -webkit-tap-highlight-color: transparent; 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "quickfix.biome": "explicit", 6 | "source.organizeImports.biome": "explicit" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from 'vue'; 2 | import App from './App.vue'; 3 | import './styles/preflight.css'; 4 | import './styles/tailwind.css'; 5 | 6 | export function createApp() { 7 | const app = createSSRApp(App); 8 | return { 9 | app, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /playground/src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages 3 | { 4 | "path": "pages/index/index", 5 | "style": { 6 | "navigationBarTitleText": "uni-app" 7 | } 8 | } 9 | ], 10 | "globalStyle": { 11 | "navigationBarTextStyle": "black", 12 | "navigationBarTitleText": "uni-app", 13 | "navigationBarBackgroundColor": "#F8F8F8", 14 | "backgroundColor": "#F8F8F8" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/src/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | -------------------------------------------------------------------------------- /packages/core/src/style/index.ts: -------------------------------------------------------------------------------- 1 | import postcss from "postcss"; 2 | import { defaultOptions } from "../options"; 3 | import { postcssTransformSelector } from "../tools"; 4 | 5 | export const isStyleFile = (fileName: string) => { 6 | return ["wxss", "acss", "jxss", "ttss", "qss", "css"].some((ext) => 7 | fileName.endsWith(ext), 8 | ); 9 | }; 10 | 11 | export const transformStyle = (source: string, options = defaultOptions) => { 12 | const processor = postcss().use(postcssTransformSelector(options)); 13 | return processor.process(source).css; 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "target": "ESNext", 8 | "resolveJsonModule": true, 9 | "paths": { 10 | "core": ["./packages/core/src/index.ts"] 11 | }, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "types": ["@dcloudio/types", "node"] 17 | }, 18 | "exclude": ["**/node_modules/**", "**/dist/**", "**/playground/**"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/script/index.ts: -------------------------------------------------------------------------------- 1 | import type { OutputChunk } from "rollup"; 2 | import { defaultOptions } from "../options"; 3 | import { type babelGetVendorExportMap, babelTransformScript } from "../tools"; 4 | 5 | export const isScriptFile = (fileName: string) => /.+\.js$/.test(fileName); 6 | 7 | export const transformScript = ( 8 | asset: OutputChunk, 9 | vendorExports: ReturnType, 10 | options = defaultOptions, 11 | ) => { 12 | const { fileName, code } = asset; 13 | if (!options.shouldTransformScript(fileName)) return code; 14 | return babelTransformScript(code, vendorExports, options); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/core/build.config.ts: -------------------------------------------------------------------------------- 1 | import { appendFileSync } from "node:fs"; 2 | import { resolve } from "node:path"; 3 | import { defineBuildConfig } from "unbuild"; 4 | 5 | export default defineBuildConfig({ 6 | entries: ["./src/index"], 7 | clean: true, 8 | declaration: true, 9 | rollup: { 10 | emitCJS: true, 11 | esbuild: { 12 | target: "node14.18", 13 | }, 14 | }, 15 | hooks: { 16 | "build:done": (ctx) => { 17 | const { outDir } = ctx.options; 18 | appendFileSync( 19 | resolve(outDir, "index.cjs"), 20 | "module.exports = Object.assign(exports.default || {}, exports)", 21 | ); 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@lerna-lite/cli/schemas/lerna-schema.json", 3 | "version": "0.15.2", 4 | "npmClient": "pnpm", 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "command": { 9 | "version": { 10 | "allowBranch": "main", 11 | "conventionalCommits": true, 12 | "changelogIncludeCommitsClientLogin": " - by @%l", 13 | "message": "chore: release %s", 14 | "remoteClient": "github" 15 | } 16 | }, 17 | "changelogPreset": "conventional-changelog-conventionalcommits", 18 | "ignoreChanges": [ 19 | "**/test/**", 20 | "**/tests/**", 21 | "**/__test__/**", 22 | "**/__tests__/**", 23 | "**/*.md" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "types": ["vite/client", "@dcloudio/types"], 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | } 17 | }, 18 | "vueCompilerOptions": { 19 | "experimentalRuntimeMode": "runtime-uni-app", 20 | "nativeTags": ["block", "component", "template", "slot"] 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 23 | } 24 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /playground/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | import { basePreset, elementPlusPreset, miniprogramBasePreset } from 'tailwind-extensions'; 3 | import { isMp, isQuickapp } from '@uni-helper/uni-env'; 4 | 5 | const presets: Config['presets'] = [basePreset]; 6 | if (isMp || isQuickapp) { 7 | presets.push( 8 | elementPlusPreset({ 9 | baseSelectors: [':root', 'page'], 10 | }), 11 | miniprogramBasePreset, 12 | ); 13 | } else { 14 | presets.push(elementPlusPreset()); 15 | } 16 | 17 | const theme: Config['theme'] = {}; 18 | if (isMp || isQuickapp) theme.screens = {}; 19 | 20 | const config: Config = { 21 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 22 | presets, 23 | theme, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/node_modules", 6 | "**/.git", 7 | "**/.DS_Store", 8 | "**/dist", 9 | "**/cache", 10 | "./playground", 11 | "./**/package.json", 12 | "./lerna.json", 13 | "./.vscode", 14 | "./coverage" 15 | ] 16 | }, 17 | "organizeImports": { 18 | "enabled": true 19 | }, 20 | "linter": { 21 | "enabled": true, 22 | "rules": { 23 | "recommended": true, 24 | "suspicious": { 25 | "noExplicitAny": "off" 26 | }, 27 | "complexity": { 28 | "noBannedTypes": "off" 29 | } 30 | } 31 | }, 32 | "formatter": { 33 | "enabled": true, 34 | "indentStyle": "space" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/template/index.ts: -------------------------------------------------------------------------------- 1 | import * as wxml from "@vivaxy/wxml"; 2 | import { defaultOptions } from "../options"; 3 | import { babelTransformClass } from "../tools"; 4 | 5 | export const isTemplateFile = (fileName: string) => { 6 | return ["wxml", "axml", "jxml", "ksml", "ttml", "qml", "xhsml", "swan"].some( 7 | (ext) => fileName.endsWith(ext), 8 | ); 9 | }; 10 | 11 | export const transformTemplate = (source: string, options = defaultOptions) => { 12 | const parsed = wxml.parse(source); 13 | wxml.traverse(parsed, (node: any) => { 14 | if (node?.type === wxml.NODE_TYPES.ELEMENT && node?.attributes) { 15 | const keys = Object.keys(node.attributes).filter((k) => 16 | options.shouldTransformAttribute(k), 17 | ); 18 | for (const k of keys) { 19 | node.attributes[k] = babelTransformClass(node.attributes[k], options); 20 | } 21 | } 22 | }); 23 | return wxml.serialize(parsed); 24 | }; 25 | -------------------------------------------------------------------------------- /playground/src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html, 7 | page { 8 | font-size: var(--font-size, 16px) !important; 9 | line-height: 1.5; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | } 14 | 15 | @layer components { 16 | .container { 17 | @apply absolute; 18 | @apply top-0; 19 | @apply left-0; 20 | @apply flex; 21 | @apply flex-col; 22 | @apply w-full; 23 | @apply h-full; 24 | } 25 | } 26 | 27 | @layer utilities { 28 | .expand { 29 | @apply relative; 30 | 31 | /* https://tailwindcss.com/docs/using-with-preprocessors#nesting */ 32 | &::after { 33 | @apply absolute; 34 | @apply -inset-2; 35 | @apply text-transparent; 36 | @apply bg-transparent; 37 | @apply border-transparent; 38 | content: ''; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.15.2](https://github.com/uni-helper/vite-plugin-uni-tailwind/compare/v0.15.1...v0.15.2) (2024-11-07) 7 | 8 | ### Bug Fixes 9 | 10 | * fix transformed quotes, close ([ce18454](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/ce18454c1a3d3273b20317912feeb1bc958dab81)) - by @ModyQyW 11 | 12 | ## [0.15.1](https://github.com/uni-helper/vite-plugin-uni-tailwind/compare/v0.15.0...v0.15.1) (2024-10-28) 13 | 14 | ### Bug Fixes 15 | 16 | * fix style judgement ([76378eb](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/76378eb60b048b075d6ad14ca9f2e7fb3f013b97)) - by @ModyQyW 17 | * fix template judgement ([fa43006](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/fa430065e69be2eb864d7ab820ca4b254df3ccd6)) - by @ModyQyW 18 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import { defineConfig } from 'vite'; 3 | import uni from '@dcloudio/vite-plugin-uni'; 4 | import tailwindcss from 'tailwindcss'; 5 | // @ts-ignore 6 | import nested from 'tailwindcss/nesting'; 7 | import postcssPresetEnv from 'postcss-preset-env'; 8 | import uniTailwind from '@uni-helper/vite-plugin-uni-tailwind'; 9 | import tailwindcssConfig from './tailwind.config.ts'; // 注意匹配实际文件 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | css: { 14 | postcss: { 15 | plugins: [ 16 | nested(), 17 | tailwindcss({ 18 | config: tailwindcssConfig, 19 | }), 20 | postcssPresetEnv({ 21 | stage: 3, 22 | features: { 'nesting-rules': false }, 23 | }), 24 | ], 25 | }, 26 | }, 27 | plugins: [ 28 | uni(), 29 | uniTailwind({ 30 | /* options */ 31 | }), 32 | ], 33 | resolve: { 34 | alias: { 35 | '@': fileURLToPath(new URL('src', import.meta.url)), 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/unocss/unocss/blob/fe83a90b59cf4599be57ea825166bb74d92b104c/.github/workflows/pkg.pr.new.yml 2 | name: Publish Any Commit 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - '**' 9 | tags: 10 | - '!**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v4 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version-file: .nvmrc 29 | cache: pnpm 30 | registry-url: https://registry.npmjs.org 31 | 32 | - name: Setup corepack 33 | run: corepack enable 34 | 35 | - name: Install Dependencies 36 | run: pnpm install 37 | 38 | - name: Build 39 | run: pnpm build 40 | 41 | - name: Release 42 | run: pnpx pkg-pr-new publish --compact --pnpm './packages/*' 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present uni-helper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/src/tools/postcss.ts: -------------------------------------------------------------------------------- 1 | import type { Declaration, Plugin, Rule } from "postcss"; 2 | import { defaultOptions } from "../options"; 3 | import { replaceCharacters, replaceElements, replaceUnicode } from "../utils"; 4 | 5 | export function postcssTransformSelector(options = defaultOptions): Plugin { 6 | return { 7 | Rule(node: Rule & { processedByPostcssTransformSelector?: boolean }) { 8 | if (node?.processedByPostcssTransformSelector) return; 9 | let newSelector = node.selector; 10 | newSelector = replaceUnicode(newSelector, "style"); 11 | newSelector = replaceElements(newSelector, options); 12 | newSelector = replaceCharacters(newSelector, "style", options); 13 | node.selector = newSelector; 14 | if (node.type === "rule" && /text--\d+rpx-/.test(node.selector)) { 15 | const index = node.nodes.findIndex( 16 | (n) => 17 | n.type === "decl" && n.prop === "color" && n.value.endsWith("rpx"), 18 | ); 19 | if (index >= 0) (node.nodes[index] as Declaration).prop = "font-size"; 20 | } 21 | node.processedByPostcssTransformSelector = true; 22 | }, 23 | postcssPlugin: 24 | "uni-helper-vite-plugin-uni-tailwind-postcss-transform-selector", 25 | }; 26 | } 27 | postcssTransformSelector.postcss = true; 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/unocss/unocss/blob/fe83a90b59cf4599be57ea825166bb74d92b104c/.github/workflows/release.yml 2 | name: Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | release: 11 | permissions: 12 | id-token: write 13 | contents: write 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version-file: .nvmrc 28 | cache: pnpm 29 | registry-url: https://registry.npmjs.org 30 | 31 | - name: Setup corepack 32 | run: corepack enable 33 | 34 | - name: GitHub Release 35 | run: pnpx changelogithub 36 | env: 37 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 38 | 39 | - name: Install Dependencies 40 | run: pnpm install 41 | 42 | - name: Build 43 | run: pnpm build 44 | 45 | - name: Publish to NPM 46 | run: pnpm -r publish --access public --no-git-checks 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 49 | NPM_CONFIG_PROVENANCE: true 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/unocss/unocss/blob/fe83a90b59cf4599be57ea825166bb74d92b104c/.github/workflows/ci.yml 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | merge_group: {} 14 | 15 | jobs: 16 | test: 17 | runs-on: ${{ matrix.os }} 18 | 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest, windows-latest] 22 | node_version: [18, 20, 22] 23 | fail-fast: false 24 | 25 | steps: 26 | - name: Set git to use LF 27 | run: | 28 | git config --global core.autocrlf false 29 | git config --global core.eol lf 30 | 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Install pnpm 35 | uses: pnpm/action-setup@v4 36 | 37 | - name: Setup Node.js ${{ matrix.node_version }} 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node_version }} 41 | cache: pnpm 42 | registry-url: https://registry.npmjs.org 43 | 44 | - name: Setup corepack 45 | run: corepack enable 46 | 47 | - name: Install Dependencies 48 | run: pnpm install --frozen-lockfile 49 | 50 | - name: Build 51 | run: pnpm build 52 | 53 | - name: Test 54 | run: pnpm test 55 | 56 | - name: Type Check 57 | run: pnpm type-check 58 | -------------------------------------------------------------------------------- /deprecated.md: -------------------------------------------------------------------------------- 1 | # 废弃 @uni-helper/vite-plugin-uni-tailwind 2 | 3 | 今日起,我们将正式废弃 @uni-helper/vite-plugin-uni-tailwind,并建议现有应用迁移至其它类似工具。 4 | 5 | ## 背景 6 | 7 | @uni-helper/vite-plugin-uni-tailwind 最初创建的目的是为了解决在 uni-app 中使用 TailwindCSS 时遇到的各种兼容性问题。 8 | 9 | 这个插件通过转换特殊字符和元素选择器,使得开发者可以在小程序中使用原生 TailwindCSS 语法进行开发,而无需调整原有的开发习惯。 10 | 11 | 由于 UnoCSS 和 WindiCSS 使用 class 时语法和 TailwindCSS 几乎完全一致,所以这个插件实际上也支持 UnoCSS 和 WindiCSS 使用 class 的情况。 12 | 13 | ## 为什么 14 | 15 | ### 更好的方案 16 | 17 | 随着社区的发展,已经出现了更加完善和活跃的解决方案: 18 | 19 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 20 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 21 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 22 | 23 | 这些工具不仅提供了与我们相似的功能,还具有更多高级特性,如: 24 | 25 | - 更好的 rpx 单位支持 26 | - 更完善的类名转换 27 | - 更强大的可配置性 28 | - 更活跃的维护和更新 29 | 30 | ### 插件实现 31 | 32 | 目前,该插件的实现存在难以解决的边界情况和性能瓶颈。在现有架构下,若不进行重大重构,短期内难以添加重要的全新功能。 33 | 34 | ### 团队重心 35 | 36 | 鉴于团队成员业余时间有限,且已将大量精力投入到公司项目及其他开源项目中,我们已难以确保本插件的维护质量及响应速度。 37 | 38 | ## 这意味着什么? 39 | 40 | 这意味着我们不会 41 | 42 | - 修改代码,包括新增功能和安全性修复; 43 | - 接受社区贡献。 44 | 45 | 但我们仍会: 46 | 47 | - 保持现有文档的可访问性。 48 | 49 | **总而言之,现有项目可以继续使用,但不保证更新相关依赖后此插件仍然能正常使用。** 50 | 51 | ## 建议 52 | 53 | 对于新项目,我们建议考虑以下替代方案: 54 | 55 | 对于 UnoCSS: 56 | 57 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 58 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 59 | 60 | 对于 TailwindCSS: 61 | 62 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 63 | 64 | 这些替代方案都有更活跃的维护团队,能够确保更可持续和可靠的开发体验。 65 | 66 | 你也可以分叉并发布自己的版本。 67 | 68 | ## 致谢 69 | 70 | 我们要衷心感谢: 71 | 72 | - 所有使用过该插件的开发者 73 | - 为项目提供反馈和建议的社区成员 74 | - 项目的贡献者们 75 | - 赞助商们的慷慨支持 76 | 77 | 同时也要特别感谢以下项目,它们为我们提供了灵感和参考: 78 | 79 | - [mini-program-tailwind](https://github.com/dcasia/mini-program-tailwind) 80 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 81 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 82 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 83 | 84 | 最后,感谢 [TailwindCSS](https://tailwindcss.com/)、[WindiCSS](https://windicss.org/) 和 [UnoCSS](https://github.com/unocss/unocss) 的开发者们,是他们让前端开发变得更加美好。 85 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uni-helper/vite-plugin-uni-tailwind", 3 | "version": "0.15.2", 4 | "description": "支持在 uni-app 中使用 TailwindCSS@3 原有语法开发", 5 | "keywords": [ 6 | "vite", 7 | "transform", 8 | "tailwind", 9 | "tailwindcss", 10 | "vite-plugin", 11 | "uni-app", 12 | "uniapp", 13 | "miniprogram", 14 | "mini-program" 15 | ], 16 | "homepage": "https://github.com/uni-helper/vite-plugin-uni-tailwind", 17 | "bugs": { 18 | "url": "https://github.com/uni-helper/vite-plugin-uni-tailwind/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/uni-helper/vite-plugin-uni-tailwind.git", 23 | "directory": "packages/core" 24 | }, 25 | "funding": "https://github.com/sponsors/modyqyw", 26 | "license": "MIT", 27 | "author": { 28 | "name": "ModyQyW", 29 | "email": "wurui.dev@gmail.com", 30 | "url": "https://modyqyw.top" 31 | }, 32 | "sideEffects": false, 33 | "type": "module", 34 | "exports": { 35 | ".": { 36 | "import": { 37 | "types": "./dist/index.d.mts", 38 | "default": "./dist/index.mjs" 39 | }, 40 | "require": { 41 | "types": "./dist/index.d.cts", 42 | "default": "./dist/index.cjs" 43 | } 44 | } 45 | }, 46 | "main": "./dist/index.cjs", 47 | "module": "./dist/index.mjs", 48 | "types": "./dist/index.d.ts", 49 | "typesVersions": { 50 | "*": { 51 | "*": [ 52 | "./dist/*", 53 | "./dist/index.d.ts" 54 | ] 55 | } 56 | }, 57 | "files": [ 58 | "dist" 59 | ], 60 | "scripts": { 61 | "build": "unbuild", 62 | "prepublishOnly": "pnpm build", 63 | "stub": "unbuild --stub" 64 | }, 65 | "dependencies": { 66 | "@babel/core": "^7.26.0", 67 | "@uni-helper/uni-env": "^0.1.5", 68 | "@vivaxy/wxml": "^2.1.0", 69 | "postcss": "^8.4.47" 70 | }, 71 | "devDependencies": { 72 | "@types/babel__core": "^7.20.5", 73 | "tailwindcss": "^3.4.14" 74 | }, 75 | "peerDependencies": { 76 | "rollup": "^2.0.0 || ^3.0.0 || ^4.0.0", 77 | "tailwindcss": "^3.0.0", 78 | "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" 79 | }, 80 | "peerDependenciesMeta": { 81 | "rollup": { 82 | "optional": true 83 | } 84 | }, 85 | "packageManager": "pnpm@9.12.3", 86 | "engines": { 87 | "node": ">=14.18" 88 | }, 89 | "publishConfig": { 90 | "access": "public", 91 | "registry": "https://registry.npmjs.org/" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /playground/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "", 3 | "appid" : "", 4 | "description" : "", 5 | "versionName" : "1.0.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | /* 5+App特有相关 */ 9 | "app-plus" : { 10 | "usingComponents" : true, 11 | "nvueStyleCompiler" : "uni-app", 12 | "compilerVersion" : 3, 13 | "splashscreen" : { 14 | "alwaysShowBeforeRender" : true, 15 | "waiting" : true, 16 | "autoclose" : true, 17 | "delay" : 0 18 | }, 19 | /* 模块配置 */ 20 | "modules" : {}, 21 | /* 应用发布信息 */ 22 | "distribute" : { 23 | /* android打包配置 */ 24 | "android" : { 25 | "permissions" : [ 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "" 41 | ] 42 | }, 43 | /* ios打包配置 */ 44 | "ios" : {}, 45 | /* SDK配置 */ 46 | "sdkConfigs" : {} 47 | } 48 | }, 49 | /* 快应用特有相关 */ 50 | "quickapp" : {}, 51 | /* 小程序特有相关 */ 52 | "mp-weixin" : { 53 | "appid" : "", 54 | "setting" : { 55 | "urlCheck" : false 56 | }, 57 | "usingComponents" : true 58 | }, 59 | "mp-alipay" : { 60 | "usingComponents" : true 61 | }, 62 | "mp-baidu" : { 63 | "usingComponents" : true 64 | }, 65 | "mp-toutiao" : { 66 | "usingComponents" : true 67 | }, 68 | "uniStatistics": { 69 | "enable": false 70 | }, 71 | "vueVersion" : "3" 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Monorepo", 6 | "keywords": [ 7 | "vite", 8 | "transform", 9 | "tailwind", 10 | "tailwindcss", 11 | "vite-plugin", 12 | "uni-app", 13 | "uniapp", 14 | "miniprogram", 15 | "mini-program" 16 | ], 17 | "homepage": "https://github.com/uni-helper/vite-plugin-uni-tailwind#readme", 18 | "bugs": { 19 | "url": "https://github.com/uni-helper/vite-plugin-uni-tailwind/issues" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/uni-helper/vite-plugin-uni-tailwind.git" 24 | }, 25 | "funding": "https://github.com/sponsors/modyqyw", 26 | "license": "MIT", 27 | "author": { 28 | "name": "ModyQyW", 29 | "email": "wurui.dev@gmail.com", 30 | "url": "https://modyqyw.top" 31 | }, 32 | "sideEffects": false, 33 | "type": "module", 34 | "scripts": { 35 | "build": "rimraf packages/*/dist --glob && pnpm -r --filter=./packages/* run build && pnpm -r run build-post", 36 | "check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", 37 | "dep-update": "taze -fw", 38 | "dev": "pnpm stub", 39 | "play:build:h5": "pnpm build && pnpm -C playground run build:h5", 40 | "play:build:mp-weixin": "pnpm build && pnpm -C playground run build:mp-weixin", 41 | "play:dev:h5": "pnpm dev && pnpm -C playground run dev:h5", 42 | "play:dev:mp-weixin": "pnpm dev && pnpm -C playground run dev:mp-weixin", 43 | "prepare": "is-ci || simple-git-hooks", 44 | "prepublishOnly": "pnpm run build", 45 | "release": "lerna version", 46 | "stub": "pnpm -r --filter=./packages/* --parallel run stub", 47 | "test": "vitest run", 48 | "test:coverage": "vitest run --coverage", 49 | "type-check": "tsc --noEmit", 50 | "preversion": "git-branch-is main && conc \"pnpm:check\" \"pnpm:type-check\" \"pnpm:test\"" 51 | }, 52 | "devDependencies": { 53 | "@biomejs/biome": "^1.9.4", 54 | "@commitlint/cli": "^19.5.0", 55 | "@commitlint/config-conventional": "^19.5.0", 56 | "@commitlint/config-pnpm-scopes": "^19.5.0", 57 | "@dcloudio/types": "^3.4.14", 58 | "@lerna-lite/cli": "^3.10.0", 59 | "@lerna-lite/version": "^3.10.0", 60 | "@types/node": "^22.9.0", 61 | "@vitest/coverage-v8": "^2.1.4", 62 | "concurrently": "^9.1.0", 63 | "conventional-changelog-conventionalcommits": "^7.0.2", 64 | "git-branch-is": "^4.0.0", 65 | "is-ci": "^3.0.1", 66 | "lint-staged": "^15.2.10", 67 | "rimraf": "^6.0.1", 68 | "simple-git-hooks": "^2.11.1", 69 | "taze": "^0.17.2", 70 | "typescript": "^5.6.3", 71 | "unbuild": "^3.0.0-rc.11", 72 | "vitest": "^2.1.4" 73 | }, 74 | "packageManager": "pnpm@9.12.3", 75 | "engines": { 76 | "node": ">=18" 77 | }, 78 | "publishConfig": { 79 | "access": "public", 80 | "registry": "https://registry.npmjs.org/" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { platform } from "@uni-helper/uni-env"; 2 | import type { OutputChunk } from "rollup"; 3 | import type * as Vite from "vite"; 4 | import { 5 | type UniTailwindPluginOptions, 6 | type UniTailwindPluginUserOptions, 7 | defaultOptions, 8 | } from "./options"; 9 | import { isScriptFile, transformScript } from "./script"; 10 | import { isStyleFile, transformStyle } from "./style"; 11 | import { isTemplateFile, transformTemplate } from "./template"; 12 | import { babelGetVendorExportMap } from "./tools"; 13 | 14 | export * from "./options"; 15 | export * from "./script"; 16 | export * from "./style"; 17 | export * from "./template"; 18 | 19 | export default function UniAppTailwindPlugin( 20 | userOptions?: UniTailwindPluginUserOptions, 21 | ): Vite.Plugin { 22 | const options: UniTailwindPluginOptions = { 23 | characterMap: userOptions?.characterMap ?? defaultOptions.characterMap, 24 | directChildrenElements: 25 | userOptions?.directChildrenElements ?? 26 | defaultOptions.directChildrenElements, 27 | divideWidthElements: 28 | userOptions?.divideWidthElements ?? defaultOptions.divideWidthElements, 29 | elementMap: userOptions?.elementMap ?? defaultOptions.elementMap, 30 | shouldApply: 31 | userOptions?.shouldApply === undefined 32 | ? defaultOptions.shouldApply 33 | : typeof userOptions?.shouldApply === "boolean" 34 | ? userOptions.shouldApply 35 | : userOptions.shouldApply(platform), 36 | shouldTransformAttribute: 37 | userOptions?.shouldTransformAttribute ?? 38 | defaultOptions.shouldTransformAttribute, 39 | shouldTransformScript: 40 | userOptions?.shouldTransformScript ?? 41 | defaultOptions.shouldTransformScript, 42 | spaceBetweenElements: 43 | userOptions?.spaceBetweenElements ?? defaultOptions.spaceBetweenElements, 44 | }; 45 | 46 | return { 47 | enforce: "post", 48 | generateBundle: (_, bundle) => { 49 | if (!options.shouldApply) return; 50 | 51 | // 解析 52 | const vendorKey = Object.keys(bundle).find((k) => 53 | k.endsWith("vendor.js"), 54 | ) as string; 55 | const vendorExportMap = babelGetVendorExportMap( 56 | bundle[vendorKey] as OutputChunk, 57 | ); 58 | 59 | // 转换 60 | for (const [fileName, asset] of Object.entries(bundle)) { 61 | if (asset.type === "chunk" && isScriptFile(fileName)) { 62 | asset.code = transformScript(asset, vendorExportMap, options); 63 | } else if (asset.type === "asset") { 64 | const { source } = asset; 65 | if (source && typeof source === "string") { 66 | let newSource = ""; 67 | if (isTemplateFile(fileName)) 68 | newSource = transformTemplate(source, options); 69 | if (isStyleFile(fileName)) 70 | newSource = transformStyle(source, options); 71 | asset.source = newSource || source; 72 | } 73 | } 74 | } 75 | }, 76 | name: "vite:uni-tailwind", 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build:app": "uni build -p app", 7 | "build:app-android": "uni build -p app-android", 8 | "build:app-ios": "uni build -p app-ios", 9 | "build:custom": "uni build -p", 10 | "build:h5": "uni build", 11 | "build:h5:ssr": "uni build --ssr", 12 | "build:mp-alipay": "uni build -p mp-alipay", 13 | "build:mp-baidu": "uni build -p mp-baidu", 14 | "build:mp-kuaishou": "uni build -p mp-kuaishou", 15 | "build:mp-lark": "uni build -p mp-lark", 16 | "build:mp-qq": "uni build -p mp-qq", 17 | "build:mp-toutiao": "uni build -p mp-toutiao", 18 | "build:mp-weixin": "uni build -p mp-weixin", 19 | "build:quickapp-webview": "uni build -p quickapp-webview", 20 | "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", 21 | "build:quickapp-webview-union": "uni build -p quickapp-webview-union", 22 | "dev:app": "uni -p app", 23 | "dev:app-android": "uni -p app-android", 24 | "dev:app-ios": "uni -p app-ios", 25 | "dev:custom": "uni -p", 26 | "dev:h5": "uni", 27 | "dev:h5:ssr": "uni --ssr", 28 | "dev:mp-alipay": "uni -p mp-alipay", 29 | "dev:mp-baidu": "uni -p mp-baidu", 30 | "dev:mp-kuaishou": "uni -p mp-kuaishou", 31 | "dev:mp-lark": "uni -p mp-lark", 32 | "dev:mp-qq": "uni -p mp-qq", 33 | "dev:mp-toutiao": "uni -p mp-toutiao", 34 | "dev:mp-weixin": "uni -p mp-weixin", 35 | "dev:quickapp-webview": "uni -p quickapp-webview", 36 | "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", 37 | "dev:quickapp-webview-union": "uni -p quickapp-webview-union" 38 | }, 39 | "dependencies": { 40 | "@dcloudio/uni-app": "3.0.0-4020920240930001", 41 | "@dcloudio/uni-app-plus": "3.0.0-4020920240930001", 42 | "@dcloudio/uni-components": "3.0.0-4020920240930001", 43 | "@dcloudio/uni-h5": "3.0.0-4020920240930001", 44 | "@dcloudio/uni-mp-alipay": "3.0.0-4020920240930001", 45 | "@dcloudio/uni-mp-baidu": "3.0.0-4020920240930001", 46 | "@dcloudio/uni-mp-kuaishou": "3.0.0-4020920240930001", 47 | "@dcloudio/uni-mp-lark": "3.0.0-4020920240930001", 48 | "@dcloudio/uni-mp-qq": "3.0.0-4020920240930001", 49 | "@dcloudio/uni-mp-toutiao": "3.0.0-4020920240930001", 50 | "@dcloudio/uni-mp-weixin": "3.0.0-4020920240930001", 51 | "@dcloudio/uni-quickapp-webview": "3.0.0-4020920240930001", 52 | "@uni-helper/uni-env": "^0.1.5", 53 | "vue": "~3.4.38", 54 | "vue-i18n": "^9.14.1" 55 | }, 56 | "devDependencies": { 57 | "@dcloudio/types": "^3.4.14", 58 | "@dcloudio/uni-automator": "3.0.0-4020920240930001", 59 | "@dcloudio/uni-cli-shared": "3.0.0-4020920240930001", 60 | "@dcloudio/uni-stacktracey": "3.0.0-4020920240930001", 61 | "@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001", 62 | "@uni-helper/vite-plugin-uni-tailwind": "workspace:*", 63 | "postcss": "^8.4.47", 64 | "postcss-preset-env": "^10.0.9", 65 | "tailwind-extensions": "^0.22.1", 66 | "tailwindcss": "^3.4.14", 67 | "typescript": "~5.5.4", 68 | "vite": "^5.4.10" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | pnpm-debug.log* 9 | run 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules 44 | jspm_packages 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules 48 | 49 | # Temp directory 50 | .temp 51 | .tmp 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache 64 | .rts2_cache_cjs 65 | .rts2_cache_es 66 | .rts2_cache_umd 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.*.test 81 | .env.local 82 | .env.*.local 83 | *.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Electron 90 | dist-electron 91 | dist_electron 92 | 93 | # Tauri 94 | dist-tauri 95 | dist_tauri 96 | 97 | # Cordova 98 | dist-cordova 99 | dist_cordova 100 | 101 | # Capacitor 102 | dist-capacitor 103 | dist_capacitor 104 | 105 | # Next.js build output 106 | .next 107 | out 108 | 109 | # Umi.js build output 110 | .umi 111 | .umi-production 112 | .umi-test 113 | 114 | # Nuxt.js build / generate output 115 | .nuxt 116 | dist 117 | 118 | # Rax.js build 119 | .rax 120 | 121 | # Vuepress build output 122 | .vuepress/dist 123 | 124 | # SSR 125 | dist-ssr 126 | dist_ssr 127 | 128 | # SSG 129 | dist-ssg 130 | dist_ssg 131 | 132 | # Serverless 133 | .serverless 134 | .dynamodb 135 | .s3 136 | .buckets 137 | .seeds 138 | 139 | # FuseBox cache 140 | .fusebox 141 | 142 | # TernJS port file 143 | .tern-port 144 | 145 | # Cypress 146 | /cypress/videos/ 147 | /cypress/screenshots/ 148 | 149 | # Editor 150 | .vscode-test 151 | .vscode/** 152 | !.vscode/extensions.json 153 | !.vscode/settings.json 154 | *.vsix 155 | .idea 156 | .hbuilder 157 | .hbuilderx 158 | *.suo 159 | *.ntvs* 160 | *.njsproj 161 | *.sln 162 | *.sw? 163 | 164 | # yarn v2 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # Apple 172 | .DS_Store 173 | *.p12 174 | *.mobileprovision 175 | 176 | # Android 177 | *.keystore 178 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { defaultOptions } from "./options"; 2 | 3 | export const replaceUnicode = (source: string, type: "style" | "template") => { 4 | if (type === "style") { 5 | return source.replace(/^\.\\(\d{2})/, (match, p1) => { 6 | const count = 7 | Number.parseInt(String.fromCodePoint(Number.parseInt(p1, 16)), 10) - 1; 8 | return `.${"x".repeat(count)}`; 9 | }); 10 | } 11 | const newSource = source.replace(/^(\d)|\s(\d)/, (match) => { 12 | const count = Number.parseInt(match, 10) - 1; 13 | return (match.startsWith(" ") ? " " : "") + "x".repeat(count); 14 | }); 15 | return newSource; 16 | }; 17 | 18 | export const replaceCharacters = ( 19 | source: string, 20 | type: "style" | "template", 21 | options = defaultOptions, 22 | ) => { 23 | let newSource = source; 24 | for (const [key, value] of options.characterMap) { 25 | const regExp = new RegExp( 26 | key.startsWith("\\") 27 | ? key 28 | : `${type === "template" ? "\\" : "\\\\\\"}${key}`, 29 | "g", 30 | ); 31 | newSource = newSource.replace(regExp, value); 32 | } 33 | return newSource; 34 | }; 35 | 36 | export const replaceElements = (source: string, options = defaultOptions) => { 37 | let newSource = source; 38 | 39 | const { 40 | directChildrenElements, 41 | divideWidthElements, 42 | elementMap, 43 | spaceBetweenElements, 44 | } = options; 45 | newSource = newSource 46 | // direct children * https://tailwindcss.com/docs/hover-focus-and-other-states#styling-direct-children 47 | .replace( 48 | /^\.(\\\*\\:.*>\s?)(\*)/, 49 | directChildrenElements.map((element) => `.$1${element}`).join(","), 50 | ) 51 | // space * https://tailwindcss.com/docs/space 52 | .replace( 53 | /^\.(-?space-\w)(-.+?)\s?>.*/, 54 | spaceBetweenElements 55 | .map((element) => `.$1$2>${element}:not([hidden]):not(:first-child)`) 56 | .join(","), 57 | ) 58 | .replace( 59 | /^\.(-?space-\w-reverse).*/, 60 | spaceBetweenElements 61 | .map((element) => `.$1>${element}:not([hidden]):not(:first-child)`) 62 | .join(","), 63 | ) 64 | // divide * https://tailwindcss.com/docs/divide-width 65 | .replace( 66 | /^\.(-?divide-\w+)(-.+?)?\s?>.*/, 67 | divideWidthElements 68 | .map((element) => `.$1$2>${element}:not([hidden]):not(:first-child)`) 69 | .join(","), 70 | ) 71 | .replace( 72 | /^\.(-?divide-\w-reverse).*/, 73 | divideWidthElements 74 | .map((element) => `.$1>${element}:not([hidden]):not(:first-child)`) 75 | .join(","), 76 | ); 77 | 78 | for (const [key, value] of elementMap) { 79 | newSource = 80 | key === "*" 81 | ? newSource.replace( 82 | new RegExp( 83 | `(? ])\\${key}(?=[\\,\\s\\0{])|(? ])\\${key}$`, 84 | ), 85 | value.join(","), 86 | ) 87 | : newSource.replace( 88 | new RegExp( 89 | `(? ])${key}(?=[\\,\\s\\0{])|(? ])${key}$`, 90 | ), 91 | value.join(","), 92 | ); 93 | } 94 | 95 | return newSource; 96 | }; 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.15.2](https://github.com/uni-helper/vite-plugin-uni-tailwind/compare/v0.15.1...v0.15.2) (2024-11-07) 7 | 8 | ### Bug Fixes 9 | 10 | * fix transformed quotes, close ([ce18454](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/ce18454c1a3d3273b20317912feeb1bc958dab81)) - by @ModyQyW 11 | 12 | ## [0.15.1](https://github.com/uni-helper/vite-plugin-uni-tailwind/compare/v0.15.0...v0.15.1) (2024-10-28) 13 | 14 | ### Bug Fixes 15 | 16 | * fix style judgement ([76378eb](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/76378eb60b048b075d6ad14ca9f2e7fb3f013b97)) - by @ModyQyW 17 | * fix template judgement ([fa43006](https://github.com/uni-helper/vite-plugin-uni-tailwind/commit/fa430065e69be2eb864d7ab820ca4b254df3ccd6)) - by @ModyQyW 18 | 19 | ## 0.15.0 (2024-03-13) 20 | 21 | * feat: 支持 `tailwindcss@3.4` [设置直接子元素样式](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-direct-children) 22 | * feat: 支持修复 `.text-[32rpx]` 样式设置,现在会正确地使用 `font-size` 而不是 `color` 23 | 24 | ## 0.14.2 (2023-11-27) 25 | 26 | * feat: `transformScript` 支持对象键转换 27 | 28 | ## 0.14.1 (2023-11-27) 29 | 30 | * fix: 修复 unicode 没有被替换的问题 31 | 32 | ## 0.14.0 (2023-11-22) 33 | 34 | * fix: 修复正则表达式不正确的问题 35 | 36 | ## 0.14.0-0 (2023-10-13) 37 | 38 | * feat: 增加 `shouldTransformScript` 选项,允许转换脚本文件 39 | * feat!: 选项 `shouldTransformTemplateAttribute` 重命名为 `shouldTransformAttribute`,该选项目前也被用于脚本文件的转换 40 | * feat!: 调整 `elementMap` 默认值,移除 `span` 的转换,`span` 目前被用于微信小程序 Skyline 引擎元素 41 | 42 | ## 0.13.1 (2023-09-27) 43 | 44 | * revert: 回滚 0.13.0 45 | 46 | ## 0.13.0 (2023-09-27) 47 | 48 | * feat: 移除 `babel`,现在直接替换模板内的相关类值 49 | 50 | ## 0.12.0 (2023-09-20) 51 | 52 | * feat: 增加 `@` 字符支持 53 | * fix: 修复字符替换顺序 54 | 55 | ## 0.11.0 (2023-09-14) 56 | 57 | * feat: 增加 `?` 字符支持 58 | 59 | ## 0.10.0 (2023-05-16) 60 | 61 | * build: 切换到 `unbuild` 62 | * feat: 增加 `shouldTransformTemplateAttribute` 选项,允许自定义更多类名支持 63 | * feat: 增加默认类名支持 64 | * feat: 增加 `$` 字符支持 65 | * feat!: 移除 `apply` 和 `getShouldApply` 选项,请使用 `shouldApply` 代替 66 | 67 | ## 0.9.1 (2023-01-04) 68 | 69 | * build: 切换到 `rollup` 70 | 71 | ## 0.9.0 (2022-12-14) 72 | 73 | * feat: 支持 vite@4 74 | * feat: 支持处理更多的类属性 75 | * feat!: 要求 `node >= 14.18` 76 | 77 | ## 0.8.1 (2022-11-16) 78 | 79 | * fix: 修复构建 80 | 81 | ## 0.8.0 (2022-11-16) 82 | 83 | * feat!: 迁移到 `@uni-helper/vite-plugin-uni-tailwind` 84 | * feat!: 要求 `node >= 14.16` 85 | 86 | ## 0.7.1 (2022-11-03) 87 | 88 | * perf: 优化默认的 `getShouldApply` 方法 89 | 90 | ## 0.7.0 (2022-10-20) 91 | 92 | * feat: 支持 `=` 和 `&` 符号转换 93 | 94 | ## 0.6.2 (2022-10-12) 95 | 96 | * fix: 修复构建 97 | 98 | ## 0.6.1 (2022-10-12) 99 | 100 | * perf: 移除多余元素映射,现在生成的样式代码更少 101 | 102 | * fix: 修复 `space-between` 和 `divide-width` 样式替换不正确的问题 103 | 104 | ## 0.6.0 (2022-09-30) 105 | 106 | * feat: 支持替换模板 `class`、`classname`、`class-name`、`*-class`、`*-classname`、`*-class-name` 中的类名 107 | * feat!: 现在要求使用 `node >= 12.2` 108 | * feat!: 现在构建目标是 `node12.2` 109 | * fix: 修复了构建不正常的问题 110 | 111 | ## 0.5.0 112 | 113 | * feat!: 移除 `*` 特殊处理,现在可以指定插件应用于哪些平台,默认为 `MP` 和 `QUICKAPP` 114 | * feat!: 更名为 `vite-plugin-uni-app-tailwind`,现在只支持 `vite` 115 | 116 | ## 0.4.1 117 | 118 | * fix: 修复编译成 `APP` 时 `uni-app` 插入的样式选择器替换不正确的问题 119 | 120 | ## 0.4.0 121 | 122 | * feat: 增加 `replaceStarSelectorPlatform` 和 `getShouldReplaceStarSelector` 选项 123 | * feat!: 增加 `*` 特殊处理,现在默认只在 `MP` 和 `QUICKAPP` 平台替换 `*` 124 | * fix: 修复选择器带有 `uni-` 前缀时仍被替换的问题 125 | * fix: 修复编译成 `APP` 时 `uni-app` 插入的样式选择器替换不正确的问题 126 | 127 | ## 0.3.1 128 | 129 | * fix: 补充元素映射 130 | 131 | ## 0.3.0 132 | 133 | * feat: 增加 `+` 转码 134 | 135 | ## 0.2.2 136 | 137 | * fix: 修复类型定义 138 | 139 | ## 0.2.1 140 | 141 | * fix: 修复构建和导出 142 | 143 | ## 0.2.0 144 | 145 | * feat!: 重命名为 `unplugin-uni-app-tailwind` 146 | * feat: 使用 `unplugin` 支持 `webpack` 147 | 148 | ## 0.1.0 149 | 150 | 同 0.0.3。 151 | 152 | ## 0.0.3 153 | 154 | * fix: 修复错误处理文件内容的问题 155 | 156 | ## 0.0.2 157 | 158 | * fix: 修复导入 159 | 160 | ## 0.0.1 161 | 162 | 初始化项目。 163 | -------------------------------------------------------------------------------- /packages/core/src/template/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { transformTemplate } from "."; 3 | 4 | describe("template", () => { 5 | it("replace []", () => { 6 | expect(transformTemplate('')).toBe( 7 | '', 8 | ); 9 | }); 10 | 11 | it("replace [] and ()", () => { 12 | expect(transformTemplate('')).toBe( 13 | '', 14 | ); 15 | }); 16 | 17 | it("replace !, [] and .", () => { 18 | expect(transformTemplate('')).toBe( 19 | '', 20 | ); 21 | }); 22 | 23 | it("replace /", () => { 24 | expect(transformTemplate('')).toBe( 25 | '', 26 | ); 27 | }); 28 | 29 | it("replace [], (), ' and /", () => { 30 | expect( 31 | transformTemplate(""), 32 | ).toBe(''); 33 | }); 34 | 35 | it("replace [] and .", () => { 36 | expect(transformTemplate('')).toBe( 37 | '', 38 | ); 39 | }); 40 | 41 | it("replace :", () => { 42 | expect(transformTemplate('')).toBe( 43 | '', 44 | ); 45 | }); 46 | 47 | it("replace [] and #", () => { 48 | expect(transformTemplate('')).toBe( 49 | '', 50 | ); 51 | }); 52 | 53 | it("replace [], (), and ,", () => { 54 | expect( 55 | transformTemplate(''), 56 | ).toBe(''); 57 | }); 58 | 59 | it("replace [] and %", () => { 60 | expect(transformTemplate('')).toBe( 61 | '', 62 | ); 63 | }); 64 | 65 | it("replace complex class", () => { 66 | expect( 67 | transformTemplate( 68 | ``, 69 | ), 70 | ).toBe( 71 | ``, 72 | ); 73 | }); 74 | 75 | it("support *-classname", () => { 76 | expect( 77 | transformTemplate(''), 78 | ).toBe(''); 79 | expect( 80 | transformTemplate( 81 | '', 82 | ), 83 | ).toBe(''); 84 | }); 85 | 86 | it("support *-class-name", () => { 87 | expect( 88 | transformTemplate(''), 89 | ).toBe(''); 90 | expect( 91 | transformTemplate( 92 | '', 93 | ), 94 | ).toBe(''); 95 | }); 96 | 97 | it("replace unicode", () => { 98 | expect(transformTemplate('')).toBe( 99 | '', 100 | ); 101 | expect(transformTemplate('')).toBe( 102 | '', 103 | ); 104 | expect( 105 | transformTemplate(''), 106 | ).toBe(''); 107 | expect( 108 | transformTemplate(''), 109 | ).toBe(''); 110 | }); 111 | 112 | it("replace *", () => { 113 | expect(transformTemplate('')).toBe( 114 | '', 115 | ); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { isMp, isQuickapp } from "@uni-helper/uni-env"; 2 | 3 | export interface UniTailwindPluginUserOptions { 4 | /** 特殊字符映射 */ 5 | characterMap?: [string, string][]; 6 | /** Direct children 元素映射 */ 7 | directChildrenElements?: string[]; 8 | /** Divide width 元素映射 */ 9 | divideWidthElements?: string[]; 10 | /** 元素映射 */ 11 | elementMap?: [string, string[]][]; 12 | /** 13 | * 是否应用该插件 14 | * 15 | * 默认编译为小程序和快应用时应用 16 | */ 17 | shouldApply?: ((currentPlatform: string) => boolean) | boolean; 18 | /** 19 | * 是否转换某个 attribute 20 | * 21 | * 默认转换以 class、Class、classname、className、ClassName、class-name 结尾的 attribute 22 | */ 23 | shouldTransformAttribute?: (attribute: string) => boolean; 24 | /** 25 | * 是否转换某个脚本文件 26 | * 27 | * 默认转换 pages、components、layouts 开头的脚本文件 28 | */ 29 | shouldTransformScript?: (fileName: string) => boolean; 30 | /** Space between 元素映射 */ 31 | spaceBetweenElements?: string[]; 32 | } 33 | 34 | export type UniTailwindPluginOptions = Omit< 35 | Required, 36 | "shouldApply" 37 | > & { 38 | /** 39 | * 是否应用该插件 40 | * 41 | * 默认编译为小程序和快应用时应用 42 | */ 43 | shouldApply: boolean; 44 | }; 45 | 46 | /** 47 | * 是否应用该插件 48 | * 49 | * 默认编译为小程序和快应用时应用 50 | */ 51 | export const defaultShouldApply = isMp || isQuickapp; 52 | 53 | /** 54 | * 是否转换某个 attribute 55 | * 56 | * 默认会转换以 class、Class、classname、className、ClassName、class-name 结尾的 attribute 57 | */ 58 | export const defaultShouldTransformAttribute = (attribute: string) => 59 | ["class", "Class", "classname", "className", "ClassName", "class-name"].some( 60 | (item) => attribute.endsWith(item), 61 | ); 62 | 63 | /** 64 | * 是否转换某个脚本文件 65 | * 66 | * 默认会转换路径以 pages、components、layouts 开头的脚本文件 67 | */ 68 | export const defaultShouldTransformScript = (fileName: string) => 69 | ["pages", "components", "layouts"].some((item) => fileName.startsWith(item)); 70 | 71 | /** 特殊字符映射 */ 72 | export const defaultCharacterMap: [string, string][] = [ 73 | ["[", "-"], 74 | ["]", "-"], 75 | ["(", "-"], 76 | [")", "-"], 77 | ["$", "-v-"], // css variable 78 | ["#", "-h-"], // hash 79 | ["!", "-i-"], // important 80 | ["/", "-s-"], // slash 81 | [".", "-d-"], // dot 82 | [":", "_"], // colon 83 | ["%", "-p-"], // percentage 84 | ["'", "-q-"], // quote 85 | ["+", "-a-"], // add 86 | ["=", "-e-"], // equal 87 | ["&", "-n-"], // and 88 | ["?", "-qm-"], // question mark 89 | ["@", "-at-"], // at 90 | ["*", "-w-"], // wildcard 91 | [",\\s", "-c-"], // comma 92 | [",", "-c-"], // comma 93 | ["\\2c\\s", "-c-"], // comma 94 | ["\\2c", "-c-"], // comma 95 | ["\\\\2c\\s", "-c-"], // comma 96 | ["\\\\2c", "-c-"], // comma 97 | ]; 98 | 99 | /** 100 | * Space between 元素映射 101 | * 102 | * https://tailwindcss.com/docs/space 103 | */ 104 | export const defaultSpaceBetweenElements = ["view", "button", "text", "image"]; 105 | 106 | /** 107 | * Divide width 元素映射 108 | * 109 | * https://tailwindcss.com/docs/divide-width 110 | */ 111 | export const defaultDivideWidthElements = ["view", "button", "text", "image"]; 112 | 113 | /** 114 | * Direct Children 元素映射 115 | * 116 | * https://tailwindcss.com/docs/hover-focus-and-other-states#styling-direct-children 117 | */ 118 | export const defaultDirectChildrenElements = [ 119 | "view", 120 | "button", 121 | "text", 122 | "image", 123 | ]; 124 | 125 | /** 元素映射 */ 126 | export const defaultElementMap: [string, string[]][] = [ 127 | ["html", ["page"]], 128 | ["body", ["page"]], 129 | ["img", ["image"]], 130 | ["a", ["functional-page-navigator", "navigator"]], 131 | [ 132 | "*", 133 | [ 134 | "page", 135 | "cover-image", 136 | "cover-view", 137 | "match-media", 138 | "movable-area", 139 | "movable-view", 140 | "scroll-view", 141 | "swiper", 142 | "swiper-item", 143 | "view", 144 | "icon", 145 | "progress", 146 | "rich-text", 147 | "text", 148 | "button", 149 | "checkbox", 150 | "checkbox-group", 151 | "editor", 152 | "form", 153 | "input", 154 | "label", 155 | "picker", 156 | "picker-view", 157 | "picker-view-column", 158 | "radio", 159 | "radio-group", 160 | "slider", 161 | "switch", 162 | "textarea", 163 | "functional-page-navigator", 164 | "navigator", 165 | "audio", 166 | "camera", 167 | "image", 168 | "live-player", 169 | "live-pusher", 170 | "video", 171 | "voip-room", 172 | "map", 173 | "canvas", 174 | "ad", 175 | "ad-custom", 176 | "official-account", 177 | "open-data", 178 | "web-view", 179 | "navigation-bar", 180 | "page-meta", 181 | ], 182 | ], 183 | ]; 184 | 185 | export const defaultOptions: UniTailwindPluginOptions = { 186 | characterMap: defaultCharacterMap, 187 | directChildrenElements: defaultDirectChildrenElements, 188 | divideWidthElements: defaultDivideWidthElements, 189 | elementMap: defaultElementMap, 190 | shouldApply: defaultShouldApply, 191 | shouldTransformAttribute: defaultShouldTransformAttribute, 192 | shouldTransformScript: defaultShouldTransformScript, 193 | spaceBetweenElements: defaultSpaceBetweenElements, 194 | } as const; 195 | -------------------------------------------------------------------------------- /packages/core/src/style/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { transformStyle } from "."; 3 | 4 | describe("style", () => { 5 | it("replace []", () => { 6 | expect(transformStyle(".w-\\[10px\\] {}")).toBe(".w--10px- {}"); 7 | }); 8 | 9 | it("replace [] and ()", () => { 10 | expect(transformStyle(".bg-\\[url\\(\\)\\] {}")).toBe(".bg--url--- {}"); 11 | }); 12 | 13 | it("replace !, [] and .", () => { 14 | expect(transformStyle(".\\!w-\\[200\\.5px\\] {}")).toBe( 15 | ".-i-w--200-d-5px- {}", 16 | ); 17 | }); 18 | 19 | it("replace /", () => { 20 | expect(transformStyle(".top-1\\/2 {}")).toBe(".top-1-s-2 {}"); 21 | }); 22 | 23 | it("replace [], (), ' and /", () => { 24 | expect( 25 | transformStyle(".bg-\\[url\\(\\'\\/img\\/grid\\.svg\\'\\)\\] {}"), 26 | ).toBe(".bg--url--q--s-img-s-grid-d-svg-q--- {}"); 27 | }); 28 | 29 | it("replace [] and .", () => { 30 | expect(transformStyle(".w-\\[200\\.5rpx\\] {}")).toBe(".w--200-d-5rpx- {}"); 31 | }); 32 | 33 | it("replace :", () => { 34 | expect(transformStyle(".sm\\:mx-auto {}")).toBe(".sm_mx-auto {}"); 35 | }); 36 | 37 | it("replace [] and #", () => { 38 | expect(transformStyle(".bg-\\[\\#fff\\] {}")).toBe(".bg---h-fff- {}"); 39 | }); 40 | 41 | it("replace [], (), and ,", () => { 42 | expect(transformStyle(".bg-\\[rgba\\(255\\,255\\,255\\,1\\)\\] {}")).toBe( 43 | ".bg--rgba-255-c-255-c-255-c-1-- {}", 44 | ); 45 | }); 46 | 47 | it("replace [] and %", () => { 48 | expect(transformStyle(".w-\\[10\\%\\] {}")).toBe(".w--10-p-- {}"); 49 | }); 50 | 51 | it("replace space-between", () => { 52 | expect( 53 | transformStyle(".space-x-0 > :not([hidden]) ~ :not([hidden]) {}"), 54 | ).toBe( 55 | ".space-x-0>view:not([hidden]):not(:first-child),.space-x-0>button:not([hidden]):not(:first-child),.space-x-0>text:not([hidden]):not(:first-child),.space-x-0>image:not([hidden]):not(:first-child) {}", 56 | ); 57 | expect(transformStyle(".space-x-0>:not([hidden])~:not([hidden]) {}")).toBe( 58 | ".space-x-0>view:not([hidden]):not(:first-child),.space-x-0>button:not([hidden]):not(:first-child),.space-x-0>text:not([hidden]):not(:first-child),.space-x-0>image:not([hidden]):not(:first-child) {}", 59 | ); 60 | expect( 61 | transformStyle(".space-x-reverse > :not([hidden]) ~ :not([hidden]) {}"), 62 | ).toBe( 63 | ".space-x-reverse>view:not([hidden]):not(:first-child),.space-x-reverse>button:not([hidden]):not(:first-child),.space-x-reverse>text:not([hidden]):not(:first-child),.space-x-reverse>image:not([hidden]):not(:first-child) {}", 64 | ); 65 | expect( 66 | transformStyle(".space-x-reverse>:not([hidden])~:not([hidden]) {}"), 67 | ).toBe( 68 | ".space-x-reverse>view:not([hidden]):not(:first-child),.space-x-reverse>button:not([hidden]):not(:first-child),.space-x-reverse>text:not([hidden]):not(:first-child),.space-x-reverse>image:not([hidden]):not(:first-child) {}", 69 | ); 70 | }); 71 | 72 | it("replace divide-width", () => { 73 | expect( 74 | transformStyle(".divide-x > :not([hidden]) ~ :not([hidden]) {}"), 75 | ).toBe( 76 | ".divide-x>view:not([hidden]):not(:first-child),.divide-x>button:not([hidden]):not(:first-child),.divide-x>text:not([hidden]):not(:first-child),.divide-x>image:not([hidden]):not(:first-child) {}", 77 | ); 78 | expect(transformStyle(".divide-x>:not([hidden])~:not([hidden]) {}")).toBe( 79 | ".divide-x>view:not([hidden]):not(:first-child),.divide-x>button:not([hidden]):not(:first-child),.divide-x>text:not([hidden]):not(:first-child),.divide-x>image:not([hidden]):not(:first-child) {}", 80 | ); 81 | expect( 82 | transformStyle(".divide-x-reverse > :not([hidden]) ~ :not([hidden]) {}"), 83 | ).toBe( 84 | ".divide-x-reverse>view:not([hidden]):not(:first-child),.divide-x-reverse>button:not([hidden]):not(:first-child),.divide-x-reverse>text:not([hidden]):not(:first-child),.divide-x-reverse>image:not([hidden]):not(:first-child) {}", 85 | ); 86 | expect( 87 | transformStyle(".divide-x-reverse>:not([hidden])~:not([hidden]) {}"), 88 | ).toBe( 89 | ".divide-x-reverse>view:not([hidden]):not(:first-child),.divide-x-reverse>button:not([hidden]):not(:first-child),.divide-x-reverse>text:not([hidden]):not(:first-child),.divide-x-reverse>image:not([hidden]):not(:first-child) {}", 90 | ); 91 | }); 92 | 93 | it("replace html", () => { 94 | expect(transformStyle("html {}")).toBe("page {}"); 95 | }); 96 | 97 | it("replace body", () => { 98 | expect(transformStyle("body {}")).toBe("page {}"); 99 | expect(transformStyle(".uv-form-item__body {}")).toBe( 100 | ".uv-form-item__body {}", 101 | ); 102 | }); 103 | 104 | it("replace img", () => { 105 | expect( 106 | transformStyle("img,svg,video,canvas,audio,iframe,embed,object {}"), 107 | ).toBe("image,svg,video,canvas,audio,iframe,embed,object {}"); 108 | expect( 109 | transformStyle( 110 | "uni-image>img{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;display:block;position:absolute;top:0;left:0;width:100%;height:100%;opacity:0}", 111 | ), 112 | ).toBe( 113 | "uni-image>img{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;display:block;position:absolute;top:0;left:0;width:100%;height:100%;opacity:0}", 114 | ); 115 | expect( 116 | transformStyle(".ql-container img{display:inline-block;max-width:100%}"), 117 | ).toBe(".ql-container img{display:inline-block;max-width:100%}"); 118 | }); 119 | 120 | it("replace a", () => { 121 | expect(transformStyle("a {}")).toBe( 122 | "functional-page-navigator,navigator {}", 123 | ); 124 | expect(transformStyle(".uv-form-item.data-v-591e6b6a {}")).toBe( 125 | ".uv-form-item.data-v-591e6b6a {}", 126 | ); 127 | expect(transformStyle("textarea {}")).toBe("textarea {}"); 128 | }); 129 | 130 | it("replace *", () => { 131 | expect(transformStyle("*, ::before, ::after {}")).toBe( 132 | "page,cover-image,cover-view,match-media,movable-area,movable-view,scroll-view,swiper,swiper-item,view,icon,progress,rich-text,text,button,checkbox,checkbox-group,editor,form,input,label,picker,picker-view,picker-view-column,radio,radio-group,slider,switch,textarea,functional-page-navigator,navigator,audio,camera,image,live-player,live-pusher,video,voip-room,map,canvas,ad,ad-custom,official-account,open-data,web-view,navigation-bar,page-meta, ::before, ::after {}", 133 | ); 134 | expect(transformStyle(".\\*\\:rounded-full>* {}")).toBe( 135 | ".-w-_rounded-full>view,.-w-_rounded-full>button,.-w-_rounded-full>text,.-w-_rounded-full>image {}", 136 | ); 137 | }); 138 | 139 | it("replace unicode", () => { 140 | expect(transformStyle(".\\32xl\\:rounded-2xl {}")).toBe( 141 | ".xxl_rounded-2xl {}", 142 | ); 143 | expect(transformStyle(".\\33xl\\:rounded-3xl {}")).toBe( 144 | ".xxxl_rounded-3xl {}", 145 | ); 146 | }); 147 | 148 | it("fix rpx", () => { 149 | expect(transformStyle(".text-\\[32rpx\\] { color: 32rpx; }")).toBe( 150 | ".text--32rpx- { font-size: 32rpx; }", 151 | ); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /packages/core/src/tools/babel.ts: -------------------------------------------------------------------------------- 1 | import * as babel from "@babel/core"; 2 | import type { PluginItem } from "@babel/core"; 3 | import type { OutputChunk } from "rollup"; 4 | import { defaultOptions } from "../options"; 5 | import { replaceCharacters, replaceUnicode } from "../utils"; 6 | 7 | type Babel = typeof babel; 8 | 9 | export const babelTransformClass = ( 10 | source: string, 11 | options = defaultOptions, 12 | ) => { 13 | // 进行匹配 14 | // eslint-disable-next-line unicorn/better-regex 15 | const ScriptsRegExp = /(\{\{)(.+?)(\}\})/g; 16 | const results = [...source.matchAll(ScriptsRegExp)]; 17 | // 没有 {{}},直接替换 18 | if (results.length === 0) { 19 | return replaceCharacters( 20 | replaceUnicode(source, "template"), 21 | "template", 22 | options, 23 | ); 24 | } 25 | // 有 {{}} 26 | // 先替换 {{}} 中内容为 ReplaceMarker 27 | const ReplaceMarker = "__VITE_PLUGIN_UNI_APP_TAILWIND__"; 28 | let newSource = source.replaceAll(ScriptsRegExp, `{{${ReplaceMarker}}}`); 29 | // 遍历处理字符 30 | for (const result of results) { 31 | // 获取 {{}} 中内容 32 | const input = result.input ?? result[0]; 33 | const inside = input.replaceAll(ScriptsRegExp, "$2"); 34 | // 处理字符 35 | const transformed = babel.transformSync(inside, { 36 | generatorOpts: { 37 | compact: true, 38 | retainLines: true, 39 | }, 40 | configFile: false, 41 | plugins: [ 42 | (instance: Babel): PluginItem => ({ 43 | visitor: { 44 | StringLiteral(path) { 45 | const raw = path.node.value; 46 | const replaced = replaceCharacters( 47 | replaceUnicode(raw, "template"), 48 | "template", 49 | options, 50 | ); 51 | if (replaced !== raw) 52 | path.replaceWith(instance.types.stringLiteral(replaced)); 53 | }, 54 | }, 55 | }), 56 | ], 57 | }); 58 | // 用处理后的内容替换 ReplaceMarker 59 | if (transformed?.code) 60 | newSource = newSource.replace( 61 | ReplaceMarker, 62 | transformed.code.replace(/;$/, "").replace(/"/g, "'"), 63 | ); 64 | } 65 | return newSource; 66 | }; 67 | 68 | export const babelGetVendorRenderPropsExportName = ( 69 | vendorAsset: OutputChunk, 70 | ) => { 71 | let name = ""; 72 | const { modules } = vendorAsset; 73 | for (const [, m] of Object.entries(modules)) { 74 | const { code } = m; 75 | if (!code || !code.includes("renderProps")) continue; 76 | const ast = babel.parseSync(code); 77 | if (!ast) continue; 78 | babel.traverse(ast, { 79 | CallExpression: (path) => { 80 | if (path.node.callee.type !== "Identifier") return; 81 | if (path.node.callee.name !== "renderProps") return; 82 | if (!path.parentPath.isArrowFunctionExpression()) return; 83 | if (!path.parentPath.parentPath.isVariableDeclarator()) return; 84 | if (path.parentPath.parentPath.node.id.type !== "Identifier") return; 85 | name = path.parentPath.parentPath.node.id.name; 86 | path.skip(); 87 | }, 88 | }); 89 | if (name) break; 90 | } 91 | return name; 92 | }; 93 | 94 | export const babelGetVendorExportMap = (vendorAsset: OutputChunk) => ({ 95 | renderProps: babelGetVendorRenderPropsExportName(vendorAsset), 96 | }); 97 | 98 | export const babelGetVendorName = (source: string) => { 99 | let name = ""; 100 | const ast = babel.parseSync(source); 101 | if (!ast) return ""; 102 | babel.traverse(ast, { 103 | StringLiteral: (path) => { 104 | if (!path.node.value.endsWith("vendor.js")) return; 105 | if (!path.parentPath.isCallExpression()) return; 106 | if (!path.parentPath.parentPath.isVariableDeclarator()) return; 107 | if (path.parentPath.parentPath.node.id.type !== "Identifier") return; 108 | name = path.parentPath.parentPath.node.id.name; 109 | path.stop(); 110 | }, 111 | }); 112 | return name; 113 | }; 114 | 115 | export const babelTransformScript = ( 116 | source: string, 117 | vendorExportMap: ReturnType, 118 | options = defaultOptions, 119 | ) => { 120 | const vendorName = babelGetVendorName(source); 121 | if (!vendorName) return source; 122 | const transformed = babel.transformSync(source, { 123 | generatorOpts: { 124 | compact: true, 125 | retainLines: true, 126 | }, 127 | configFile: false, 128 | plugins: [ 129 | (instance: Babel): PluginItem => ({ 130 | visitor: { 131 | StringLiteral: (path) => { 132 | if (!path.parentPath.isObjectProperty()) return; 133 | if (!path.parentPath.parentPath.isObjectExpression()) return; 134 | if ( 135 | !path.parentPath.parentPath.parentPath.isCallExpression() && 136 | !path.parentPath.parentPath.parentPath.isObjectProperty() 137 | ) 138 | return; 139 | if (path.parentPath.parentPath.parentPath.isCallExpression()) { 140 | if ( 141 | path.parentPath.parentPath.parentPath.node.callee.type !== 142 | "MemberExpression" 143 | ) 144 | return; 145 | if ( 146 | path.parentPath.parentPath.parentPath.node.callee.object 147 | .type !== "Identifier" 148 | ) 149 | return; 150 | if ( 151 | path.parentPath.parentPath.parentPath.node.callee.object 152 | .name !== vendorName 153 | ) 154 | return; 155 | if ( 156 | path.parentPath.parentPath.parentPath.node.callee.property 157 | .type !== "Identifier" 158 | ) 159 | return; 160 | if ( 161 | path.parentPath.parentPath.parentPath.node.callee.property 162 | .name !== vendorExportMap.renderProps 163 | ) 164 | return; 165 | if (path.parentPath.node.key.type !== "StringLiteral") return; 166 | if ( 167 | !options.shouldTransformAttribute( 168 | path.parentPath.node.key.value, 169 | ) 170 | ) 171 | return; 172 | } else { 173 | if ( 174 | path.parentPath.parentPath.parentPath.node.key.type !== 175 | "StringLiteral" 176 | ) 177 | return; 178 | if ( 179 | !path.parentPath.parentPath.parentPath.parentPath.isObjectExpression() 180 | ) 181 | return; 182 | if ( 183 | !path.parentPath.parentPath.parentPath.parentPath.parentPath.isCallExpression() 184 | ) 185 | return; 186 | if ( 187 | path.parentPath.parentPath.parentPath.parentPath.parentPath.node 188 | .callee.type !== "MemberExpression" 189 | ) 190 | return; 191 | if ( 192 | path.parentPath.parentPath.parentPath.parentPath.parentPath.node 193 | .callee.object.type !== "Identifier" 194 | ) 195 | return; 196 | if ( 197 | path.parentPath.parentPath.parentPath.parentPath.parentPath.node 198 | .callee.object.name !== vendorName 199 | ) 200 | return; 201 | if ( 202 | path.parentPath.parentPath.parentPath.parentPath.parentPath.node 203 | .callee.property.type !== "Identifier" 204 | ) 205 | return; 206 | if ( 207 | path.parentPath.parentPath.parentPath.parentPath.parentPath.node 208 | .callee.property.name !== vendorExportMap.renderProps 209 | ) 210 | return; 211 | if ( 212 | !options.shouldTransformAttribute( 213 | path.parentPath.parentPath.parentPath.node.key.value, 214 | ) 215 | ) 216 | return; 217 | } 218 | const raw = path.node.value; 219 | const replaced = replaceCharacters( 220 | replaceUnicode(raw, "template"), 221 | "template", 222 | options, 223 | ); 224 | if (replaced !== raw) 225 | path.replaceWith(instance.types.stringLiteral(replaced)); 226 | }, 227 | }, 228 | }), 229 | ], 230 | }); 231 | return transformed?.code ?? source; 232 | }; 233 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @uni-helper/vite-plugin-uni-tailwind 2 | 3 | 14 | 15 | 支持在 uni-app 中使用 TailwindCSS v3 原有语法开发小程序。支持 Vite v2 ~ v5,要求 `node>=14.18`。 16 | 17 | ## 使用 18 | 19 | 参考 [TailwindCSS 文档](https://tailwindcss.com/) 在 `uni-app` 项目中安装配置 `TailwindCSS`。你无需禁用 `preflight`,也无需调整原有语法(如 `.w-[200.5rpx]` 等),你只需要正常书写类名,该插件会替你处理剩下的事情。 20 | 21 | 安装依赖。 22 | 23 | ```shell 24 | npm install @uni-helper/vite-plugin-uni-tailwind -D 25 | ``` 26 | 27 | 配置 `vite.config.ts`。 28 | 29 | ```typescript 30 | import { defineConfig } from 'vite'; 31 | // @ts-ignore 32 | import nested from 'tailwindcss/nesting'; 33 | import tailwindcss from 'tailwindcss'; 34 | import tailwindcssConfig from './tailwind.config.ts'; // 注意匹配实际文件 35 | import postcssPresetEnv from 'postcss-preset-env'; 36 | import uni from '@dcloudio/vite-plugin-uni'; 37 | import uniTailwind from '@uni-helper/vite-plugin-uni-tailwind'; 38 | 39 | // https://vitejs.dev/config/ 40 | export default defineConfig({ 41 | css: { 42 | // 只能在 Vite 配置文件内处理 postcss 配置 43 | // https://github.com/dcloudio/uni-app/issues/3367 44 | postcss: { 45 | plugins: [ 46 | nested(), 47 | tailwindcss({ 48 | config: tailwindcssConfig, 49 | }), 50 | postcssPresetEnv({ 51 | stage: 3, 52 | features: { 'nesting-rules': false }, 53 | }), 54 | ], 55 | }, 56 | }, 57 | plugins: [ 58 | uni(), 59 | uniTailwind({ 60 | /* options */ 61 | }), 62 | ], 63 | }); 64 | ``` 65 | 66 | 完整示例请查看 [playground](https://github.com/uni-helper/vite-plugin-uni-tailwind/tree/main/playground)。 67 | 68 | ## 配置项 `Options` 69 | 70 | ### `shouldApply` 71 | 72 | - 类型:`boolean | ((currentPlatform: string) => boolean)` 73 | - 默认值:`编译为小程序和快应用时应用` 74 | 75 | 是否应用该插件。 76 | 77 | `APP` 使用 `WebView` 运行,`H5` 使用浏览器运行,基本都支持特殊字符,所以默认编译为小程序和快应用时应用该插件。 78 | 79 | ### `shouldTransformAttribute` 80 | 81 | - 类型:`(attribute: string) => boolean` 82 | - 默认值:`转换以 class、Class、classname、className、ClassName、class-name 结尾的 attribute` 83 | 84 | 是否转换某个 `attribute`。 85 | 86 | ### `shouldTransformScript` 87 | 88 | - 类型:`(fileName: string) => boolean` 89 | - 默认值:`转换路径以 pages、components、layouts 开头的脚本文件` 90 | 91 | 是否转换某个脚本文件。 92 | 93 | ### `characterMap` 94 | 95 | - 类型:`[string, string][]` 96 | - 默认值如下 97 | 98 | ```typescript 99 | [ 100 | ['[', '-'], 101 | [']', '-'], 102 | ['(', '-'], 103 | [')', '-'], 104 | ['$', '-v-'], // css variable 105 | ['#', '-h-'], // hash 106 | ['!', '-i-'], // important 107 | ['/', '-s-'], // slash 108 | ['.', '-d-'], // dot 109 | [':', '_'], // colon 110 | ['%', '-p-'], // percentage 111 | ["'", '-q-'], // quote 112 | ['+', '-a-'], // add 113 | ['=', '-e-'], // equal 114 | ['&', '-n-'], // and 115 | ['?', '-qm-'], // question mark 116 | ['@', '-at-'], // at 117 | ['*', '-w-'], // wildcard 118 | [',\\s', '-c-'], // comma 119 | [',', '-c-'], // comma 120 | ['\\2c\\s', '-c-'], // comma 121 | ['\\2c', '-c-'], // comma 122 | ['\\\\2c\\s', '-c-'], // comma 123 | ['\\\\2c', '-c-'], // comma 124 | ]; 125 | ``` 126 | 127 | 所有生成样式中特殊字符需要替换成什么字符串。 128 | 129 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 130 | 131 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 132 | 133 | ### `spaceBetweenElements` 134 | 135 | - 类型:`string[]` 136 | - 默认值:`['view', 'button', 'text', 'image']` 137 | 138 | [Space Between](https://tailwindcss.com/docs/space) 生成样式中,`*` 需要替换成什么元素。 139 | 140 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 141 | 142 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 143 | 144 | ### `divideWidthElements` 145 | 146 | - 类型:`string[]` 147 | - 默认值:`['view', 'button', 'text', 'image']` 148 | 149 | [Divide Width](https://tailwindcss.com/docs/divide-width) 生成样式中,`*` 需要替换成什么元素。 150 | 151 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 152 | 153 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 154 | 155 | ### `directChildrenElements` 156 | 157 | - 类型:`string[]` 158 | - 默认值:`['view', 'button', 'text', 'image']` 159 | 160 | [Direct Children](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-direct-children) 生成样式中,后一个 `*` 需要替换成什么元素。 161 | 162 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 163 | 164 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 165 | 166 | ### `elementMap` 167 | 168 | - 类型:`[string, string[]][]` 169 | - 默认值如下 170 | 171 | ```typescript 172 | [ 173 | ['html', ['page']], 174 | ['body', ['page']], 175 | ['img', ['image']], 176 | ['a', ['functional-page-navigator', 'navigator']], 177 | [ 178 | '*', 179 | [ 180 | 'page', 181 | 'cover-image', 182 | 'cover-view', 183 | 'match-media', 184 | 'movable-area', 185 | 'movable-view', 186 | 'scroll-view', 187 | 'swiper', 188 | 'swiper-item', 189 | 'view', 190 | 'icon', 191 | 'progress', 192 | 'rich-text', 193 | 'text', 194 | 'button', 195 | 'checkbox', 196 | 'checkbox-group', 197 | 'editor', 198 | 'form', 199 | 'input', 200 | 'label', 201 | 'picker', 202 | 'picker-view', 203 | 'picker-view-column', 204 | 'radio', 205 | 'radio-group', 206 | 'slider', 207 | 'switch', 208 | 'textarea', 209 | 'functional-page-navigator', 210 | 'navigator', 211 | 'audio', 212 | 'camera', 213 | 'image', 214 | 'live-player', 215 | 'live-pusher', 216 | 'video', 217 | 'voip-room', 218 | 'map', 219 | 'canvas', 220 | 'ad', 221 | 'ad-custom', 222 | 'official-account', 223 | 'open-data', 224 | 'web-view', 225 | 'navigation-bar', 226 | 'page-meta', 227 | ], 228 | ], 229 | ]; 230 | ``` 231 | 232 | 所有生成样式中特定元素需要替换成什么元素。 233 | 234 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 235 | 236 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 237 | 238 | ## FAQ 239 | 240 | ### 可以支持 WindiCSS 吗? 241 | 242 | **请注意:请不要在新项目中使用 WindiCSS!详见 [Windi CSS is Sunsetting](https://windicss.org/posts/sunsetting.html)。** 243 | 244 | 如果你没有使用 WindiCSS 内的高级功能(如 [Attributify Mode](https://windicss.org/features/attributify.html)),这个库可以正常工作。 245 | 246 | ### 可以支持 UnoCSS 吗? 247 | 248 | **建议使用 [unocss-applet](https://github.com/unocss-applet/unocss-applet) 或 [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 以获取更好的支持。** 249 | 250 | 如果你没有使用 UnoCSS 内的高级功能(如 [Attributify Mode](https://unocss.dev/presets/attributify)、[Tagify Mode](https://unocss.dev/presets/tagify)),这个库可以正常工作。 251 | 252 | ### 可以支持 rpx 转换吗? 253 | 254 | 引自 [微信小程序文档](https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html#%E5%B0%BA%E5%AF%B8%E5%8D%95%E4%BD%8D): 255 | 256 | > rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。 257 | 258 | 简而言之,rpx 是一个跟屏幕宽度挂钩的响应式单位,不应该也不需要把所有用到 px 或者 rem 的地方换成 rpx。 259 | 260 | 什么时候必须要用 rpx?我个人的经验是侧边栏的宽度需要随屏幕宽度变化、页面主体根据侧边栏宽度变化时,才必须用到 rpx + flexbox 的组合,否则用 flexbox 就已经足够了。 261 | 262 | 所以,这个插件不支持 rpx 转换。你可以直接 [使用任意值](https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values),如 `.w-[750rpx]`、`.w-[200rpx]`,我相信可以满足绝大部分的需求。 263 | 264 | 如果你悲伤地发现这没法满足你的需求,可能这个插件不适合你,请看看以下几个项目是否满足你的需求。 265 | 266 | - [tailwind-extensions](https://www.npmjs.com/package/tailwind-extensions) 267 | - [mini-program-tailwind](https://github.com/dcasia/mini-program-tailwind) 268 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 269 | - [unocss](https://unocss.dev) 270 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 271 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 272 | 273 | ### 这个插件的原理是什么? 274 | 275 | uni-app + TailwindCSS 不能编译出小程序能正常运行的代码的错误原因有以下几种: 276 | 277 | - 样式文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等; 278 | - 样式文件中含有不支持的元素,如 `html`, `body`、`img`、`a`、`*` 等; 279 | - 自带组件传参,模板文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等; 280 | - 自定义组件传参,脚本文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等,导致参数渲染不正常。 281 | 282 | 那么,我们只需要做到以下几点就可以让 TailwindCSS 跑在小程序中,而不需要调整 TailwindCSS 的语法来增加开发时的心智负担: 283 | 284 | - 使用 PostCSS 改写样式文件里面的 `selector`,包括字符和元素; 285 | - 使用 Babel 改写模板文件里面的 `class`,只包括字符,这是为了和样式文件里面的 `selector` 相匹配; 286 | - 使用 Babel 改写脚本文件里面的 `class`,这也是为了和样式文件里面的 `selector` 相匹配。 287 | 288 | 但请注意,这个插件不是万能的。 289 | 290 | - 插件无法突破小程序的限制,比如 `bg-[url(xxxx)]` 经过插件处理后可以正常使用,但是小程序平台不支持使用 `background-image`,此时仍然无法正常渲染出图片。 291 | - 插件不支持特别复杂的情况,如果自定义组件传参过于复杂,仍然可能绕过插件处理。如果你发现这种情况,欢迎提交 Issue 或 PR 协助改进该插件,非常感谢!🙏 292 | 293 | ## 资源 294 | 295 | - [改动日志](https://github.com/uni-helper/vite-plugin-uni-tailwind/tree/main/CHANGELOG.md) 296 | 297 | ## 致谢 298 | 299 | 该项目从以下项目汲取了灵感并参考了代码。在此对它们的开发者表示由衷的感谢。 300 | 301 | - [mini-program-tailwind](https://github.com/dcasia/mini-program-tailwind) 302 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 303 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 304 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 305 | 306 | 也感谢以下项目的开发者,如果没有他们,前端开发比现在更加困难。 307 | 308 | - [TailwindCSS](https://tailwindcss.com/) 309 | - [WindiCSS](https://windicss.org/) 310 | - [UnoCSS](https://github.com/unocss/unocss) 311 | 312 | ## 贡献者们 313 | 314 | 该项目由 [ModyQyW](https://github.com/ModyQyW) 创建。 315 | 316 | 感谢 [所有贡献者](https://github.com/uni-helper/vite-plugin-uni-tailwind/graphs/contributors) 的付出! 317 | 318 | ## 赞助 319 | 320 | 如果这个包对你有所帮助,请考虑 [赞助](https://github.com/ModyQyW/sponsors) 支持,这将有利于项目持续开发和维护。 321 | 322 |

323 | 324 | 325 | 326 |

327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @uni-helper/vite-plugin-uni-tailwind 2 | 3 | **今日起,我们将正式废弃 @uni-helper/vite-plugin-uni-tailwind,并建议现有应用迁移到其它类似工具。详情请阅读 [废弃 @uni-helper/vite-plugin-uni-tailwind](./deprecated.md)。** 4 | 5 | 16 | 17 | 支持在 uni-app 中使用 TailwindCSS v3 原有语法开发小程序。支持 Vite v2 ~ v5,要求 `node>=14.18`。 18 | 19 | ## 使用 20 | 21 | 参考 [TailwindCSS 文档](https://tailwindcss.com/) 在 `uni-app` 项目中安装配置 `TailwindCSS`。你无需禁用 `preflight`,也无需调整原有语法(如 `.w-[200.5rpx]` 等),你只需要正常书写类名,该插件会替你处理剩下的事情。 22 | 23 | 安装依赖。 24 | 25 | ```shell 26 | npm install @uni-helper/vite-plugin-uni-tailwind -D 27 | ``` 28 | 29 | 配置 `vite.config.ts`。 30 | 31 | ```typescript 32 | import { defineConfig } from 'vite'; 33 | // @ts-ignore 34 | import tailwindcssNesting from 'tailwindcss/nesting'; 35 | import tailwindcss from 'tailwindcss'; 36 | import tailwindcssConfig from './tailwind.config.ts'; // 注意匹配实际文件 37 | import postcssPresetEnv from 'postcss-preset-env'; 38 | import uni from '@dcloudio/vite-plugin-uni'; 39 | import uniTailwind from '@uni-helper/vite-plugin-uni-tailwind'; 40 | 41 | // https://vitejs.dev/config/ 42 | export default defineConfig({ 43 | css: { 44 | // 只能在 Vite 配置文件内处理 postcss 配置 45 | // https://github.com/dcloudio/uni-app/issues/3367 46 | postcss: { 47 | plugins: [ 48 | tailwindcssNesting(), 49 | tailwindcss({ 50 | config: tailwindcssConfig, 51 | }), 52 | postcssPresetEnv({ 53 | stage: 3, 54 | features: { 55 | // 避免处理冲突 56 | 'nesting-rules': false, 57 | // 强制处理全局和局部 CSS Variables,无视浏览器兼容性 58 | 'custom-properties': true, 59 | // 强制处理 @layer 规则,无视浏览器兼容性 60 | 'cascade-layers': true, 61 | // 更多用法见 postcss-preset-env 文档 62 | }, 63 | }), 64 | ], 65 | }, 66 | }, 67 | plugins: [ 68 | uni(), 69 | uniTailwind({ 70 | /* options */ 71 | }), 72 | ], 73 | }); 74 | ``` 75 | 76 | 完整示例请查看 [playground](https://github.com/uni-helper/vite-plugin-uni-tailwind/tree/main/playground)。 77 | 78 | ## 配置项 `Options` 79 | 80 | ### `shouldApply` 81 | 82 | - 类型:`boolean | ((currentPlatform: string) => boolean)` 83 | - 默认值:`编译为小程序和快应用时应用` 84 | 85 | 是否应用该插件。 86 | 87 | `APP` 使用 `WebView` 运行,`H5` 使用浏览器运行,基本都支持特殊字符,所以默认编译为小程序和快应用时应用该插件。 88 | 89 | ### `shouldTransformAttribute` 90 | 91 | - 类型:`(attribute: string) => boolean` 92 | - 默认值:`转换以 class、Class、classname、className、ClassName、class-name 结尾的 attribute` 93 | 94 | 是否转换某个 `attribute`。 95 | 96 | ### `shouldTransformScript` 97 | 98 | - 类型:`(fileName: string) => boolean` 99 | - 默认值:`转换路径以 pages、components、layouts 开头的脚本文件` 100 | 101 | 是否转换某个脚本文件。 102 | 103 | ### `characterMap` 104 | 105 | - 类型:`[string, string][]` 106 | - 默认值如下 107 | 108 | ```typescript 109 | [ 110 | ['[', '-'], 111 | [']', '-'], 112 | ['(', '-'], 113 | [')', '-'], 114 | ['$', '-v-'], // css variable 115 | ['#', '-h-'], // hash 116 | ['!', '-i-'], // important 117 | ['/', '-s-'], // slash 118 | ['.', '-d-'], // dot 119 | [':', '_'], // colon 120 | ['%', '-p-'], // percentage 121 | ["'", '-q-'], // quote 122 | ['+', '-a-'], // add 123 | ['=', '-e-'], // equal 124 | ['&', '-n-'], // and 125 | ['?', '-qm-'], // question mark 126 | ['@', '-at-'], // at 127 | ['*', '-w-'], // wildcard 128 | [',\\s', '-c-'], // comma 129 | [',', '-c-'], // comma 130 | ['\\2c\\s', '-c-'], // comma 131 | ['\\2c', '-c-'], // comma 132 | ['\\\\2c\\s', '-c-'], // comma 133 | ['\\\\2c', '-c-'], // comma 134 | ]; 135 | ``` 136 | 137 | 所有生成样式中特殊字符需要替换成什么字符串。 138 | 139 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 140 | 141 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 142 | 143 | ### `spaceBetweenElements` 144 | 145 | - 类型:`string[]` 146 | - 默认值:`['view', 'button', 'text', 'image']` 147 | 148 | [Space Between](https://tailwindcss.com/docs/space) 生成样式中,`*` 需要替换成什么元素。 149 | 150 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 151 | 152 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 153 | 154 | ### `divideWidthElements` 155 | 156 | - 类型:`string[]` 157 | - 默认值:`['view', 'button', 'text', 'image']` 158 | 159 | [Divide Width](https://tailwindcss.com/docs/divide-width) 生成样式中,`*` 需要替换成什么元素。 160 | 161 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 162 | 163 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 164 | 165 | ### `directChildrenElements` 166 | 167 | - 类型:`string[]` 168 | - 默认值:`['view', 'button', 'text', 'image']` 169 | 170 | [Direct Children](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-direct-children) 生成样式中,后一个 `*` 需要替换成什么元素。 171 | 172 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 173 | 174 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 175 | 176 | ### `elementMap` 177 | 178 | - 类型:`[string, string[]][]` 179 | - 默认值如下 180 | 181 | ```typescript 182 | [ 183 | ['html', ['page']], 184 | ['body', ['page']], 185 | ['img', ['image']], 186 | ['a', ['functional-page-navigator', 'navigator']], 187 | [ 188 | '*', 189 | [ 190 | 'page', 191 | 'cover-image', 192 | 'cover-view', 193 | 'match-media', 194 | 'movable-area', 195 | 'movable-view', 196 | 'scroll-view', 197 | 'swiper', 198 | 'swiper-item', 199 | 'view', 200 | 'icon', 201 | 'progress', 202 | 'rich-text', 203 | 'text', 204 | 'button', 205 | 'checkbox', 206 | 'checkbox-group', 207 | 'editor', 208 | 'form', 209 | 'input', 210 | 'label', 211 | 'picker', 212 | 'picker-view', 213 | 'picker-view-column', 214 | 'radio', 215 | 'radio-group', 216 | 'slider', 217 | 'switch', 218 | 'textarea', 219 | 'functional-page-navigator', 220 | 'navigator', 221 | 'audio', 222 | 'camera', 223 | 'image', 224 | 'live-player', 225 | 'live-pusher', 226 | 'video', 227 | 'voip-room', 228 | 'map', 229 | 'canvas', 230 | 'ad', 231 | 'ad-custom', 232 | 'official-account', 233 | 'open-data', 234 | 'web-view', 235 | 'navigation-bar', 236 | 'page-meta', 237 | ], 238 | ], 239 | ]; 240 | ``` 241 | 242 | 所有生成样式中特定元素需要替换成什么元素。 243 | 244 | 如果不替换,可能会导致无法正常运行。如果确认无需替换,请设置为空数组。 245 | 246 | 替换顺序:`directChildrenElements` -> `spaceBetweenElements` -> `divideWidthElements` -> `characterMap` -> `elementMap`。 247 | 248 | ## FAQ 249 | 250 | ### 可以支持 WindiCSS 吗? 251 | 252 | **请注意:请不要在新项目中使用 WindiCSS!详见 [Windi CSS is Sunsetting](https://windicss.org/posts/sunsetting.html)。** 253 | 254 | 如果你没有使用 WindiCSS 内的高级功能(如 [Attributify Mode](https://windicss.org/features/attributify.html)),这个库可以正常工作。 255 | 256 | ### 可以支持 UnoCSS 吗? 257 | 258 | **建议使用 [unocss-applet](https://github.com/unocss-applet/unocss-applet) 或 [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 以获取更好的支持。** 259 | 260 | 如果你没有使用 UnoCSS 内的高级功能(如 [Attributify Mode](https://unocss.dev/presets/attributify)、[Tagify Mode](https://unocss.dev/presets/tagify)),这个库可以正常工作。 261 | 262 | ### 可以支持 rpx 转换吗? 263 | 264 | 引自 [微信小程序文档](https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html#%E5%B0%BA%E5%AF%B8%E5%8D%95%E4%BD%8D): 265 | 266 | > rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。 267 | 268 | 简而言之,rpx 是一个跟屏幕宽度挂钩的响应式单位,不应该也不需要把所有用到 px 或者 rem 的地方换成 rpx。 269 | 270 | 什么时候必须要用 rpx?我个人的经验是侧边栏的宽度需要随屏幕宽度变化、页面主体根据侧边栏宽度变化时,才必须用到 rpx + flexbox 的组合,否则用 flexbox 就已经足够了。 271 | 272 | 所以,这个插件不支持 rpx 转换。你可以直接 [使用任意值](https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values),如 `.w-[750rpx]`、`.w-[200rpx]`,我相信可以满足绝大部分的需求。 273 | 274 | 如果你悲伤地发现这没法满足你的需求,可能这个插件不适合你,请看看以下几个项目是否满足你的需求。 275 | 276 | - [tailwind-extensions](https://www.npmjs.com/package/tailwind-extensions) 277 | - [mini-program-tailwind](https://github.com/dcasia/mini-program-tailwind) 278 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 279 | - [unocss](https://unocss.dev) 280 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 281 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 282 | 283 | ### 这个插件的原理是什么? 284 | 285 | uni-app + TailwindCSS 不能编译出小程序能正常运行的代码的错误原因有以下几种: 286 | 287 | - 样式文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等; 288 | - 样式文件中含有不支持的元素,如 `html`, `body`、`img`、`a`、`*` 等; 289 | - 自带组件传参,模板文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等; 290 | - 自定义组件传参,脚本文件中含有不支持的字符,如 `()[]$#!/.:,%'` 等,导致参数渲染不正常。 291 | 292 | 那么,我们只需要做到以下几点就可以让 TailwindCSS 跑在小程序中,而不需要调整 TailwindCSS 的语法来增加开发时的心智负担: 293 | 294 | - 使用 PostCSS 改写样式文件里面的 `selector`,包括字符和元素; 295 | - 使用 Babel 改写模板文件里面的 `class`,只包括字符,这是为了和样式文件里面的 `selector` 相匹配; 296 | - 使用 Babel 改写脚本文件里面的 `class`,这也是为了和样式文件里面的 `selector` 相匹配。 297 | 298 | 但请注意,这个插件不是万能的。 299 | 300 | - 插件无法突破小程序的限制,比如 `bg-[url(xxxx)]` 经过插件处理后可以正常使用,但是小程序平台不支持使用 `background-image`,此时仍然无法正常渲染出图片。 301 | - 插件不支持特别复杂的情况,如果自定义组件传参过于复杂,仍然可能绕过插件处理。如果你发现这种情况,欢迎提交 Issue 或 PR 协助改进该插件,非常感谢!🙏 302 | 303 | ## 资源 304 | 305 | - [改动日志](https://github.com/uni-helper/vite-plugin-uni-tailwind/tree/main/CHANGELOG.md) 306 | 307 | ## 致谢 308 | 309 | 该项目从以下项目汲取了灵感并参考了代码。在此对它们的开发者表示由衷的感谢。 310 | 311 | - [mini-program-tailwind](https://github.com/dcasia/mini-program-tailwind) 312 | - [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss) 313 | - [unocss-applet](https://github.com/unocss-applet/unocss-applet) 314 | - [unocss-preset-weapp](https://github.com/MellowCo/unocss-preset-weapp) 315 | 316 | 也感谢以下项目的开发者,如果没有他们,前端开发比现在更加困难。 317 | 318 | - [TailwindCSS](https://tailwindcss.com/) 319 | - [WindiCSS](https://windicss.org/) 320 | - [UnoCSS](https://github.com/unocss/unocss) 321 | 322 | ## 贡献者们 323 | 324 | 该项目由 [ModyQyW](https://github.com/ModyQyW) 创建。 325 | 326 | 感谢 [所有贡献者](https://github.com/uni-helper/vite-plugin-uni-tailwind/graphs/contributors) 的付出! 327 | 328 | ## 赞助 329 | 330 | 如果这个包对你有所帮助,请考虑 [赞助](https://github.com/ModyQyW/sponsors) 支持,这将有利于项目持续开发和维护。 331 | 332 |

333 | 334 | 335 | 336 |

337 | -------------------------------------------------------------------------------- /packages/core/src/tools/babel.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | babelGetVendorName, 4 | babelTransformClass, 5 | babelTransformScript, 6 | } from "."; 7 | 8 | describe("babel", () => { 9 | it("babelTransformClass", () => { 10 | expect(babelTransformClass("w-[0.5px]")).toBe("w--0-d-5px-"); 11 | expect(babelTransformClass("bg-[url()]")).toBe("bg--url---"); 12 | expect(babelTransformClass("!w-[200.5px]")).toBe("-i-w--200-d-5px-"); 13 | expect(babelTransformClass("top-1/2")).toBe("top-1-s-2"); 14 | expect(babelTransformClass("bg-[url('/img/grid.svg')]")).toBe( 15 | "bg--url--q--s-img-s-grid-d-svg-q---", 16 | ); 17 | expect(babelTransformClass("w-[200.5rpx]")).toBe("w--200-d-5rpx-"); 18 | expect(babelTransformClass("sm:mx-auto")).toBe("sm_mx-auto"); 19 | expect(babelTransformClass("bg-[#fff]")).toBe("bg---h-fff-"); 20 | expect(babelTransformClass("bg-[rgba(255,255,255,1)]")).toBe( 21 | "bg--rgba-255-c-255-c-255-c-1--", 22 | ); 23 | expect(babelTransformClass("w-[10%]")).toBe("w--10-p--"); 24 | expect( 25 | babelTransformClass(`{{['w-[10%]','bg-[#fff]',virtualHostClass]}}`), 26 | ).toBe(`{{['w--10-p--','bg---h-fff-',virtualHostClass]}}`); 27 | expect(babelTransformClass("*:rounded-full")).toBe("-w-_rounded-full"); 28 | }); 29 | 30 | it("babelGetVendorName", () => { 31 | expect( 32 | babelGetVendorName(`"use strict"; 33 | var __defProp = Object.defineProperty; 34 | var __defProps = Object.defineProperties; 35 | var __getOwnPropDescs = Object.getOwnPropertyDescriptors; 36 | var __getOwnPropSymbols = Object.getOwnPropertySymbols; 37 | var __hasOwnProp = Object.prototype.hasOwnProperty; 38 | var __propIsEnum = Object.prototype.propertyIsEnumerable; 39 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 40 | var __spreadValues = (a, b) => { 41 | for (var prop in b || (b = {})) 42 | if (__hasOwnProp.call(b, prop)) 43 | __defNormalProp(a, prop, b[prop]); 44 | if (__getOwnPropSymbols) 45 | for (var prop of __getOwnPropSymbols(b)) { 46 | if (__propIsEnum.call(b, prop)) 47 | __defNormalProp(a, prop, b[prop]); 48 | } 49 | return a; 50 | }; 51 | var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); 52 | const common_vendor = require("../common/vendor.js"); 53 | const __unplugin_components_0 = () => "../components/v-status-bar/v-status-bar.js"; 54 | if (!Array) { 55 | const _easycom_v_status_bar2 = __unplugin_components_0; 56 | _easycom_v_status_bar2(); 57 | } 58 | const _easycom_v_status_bar = () => "../components/v-status-bar/v-status-bar.js"; 59 | if (!Math) { 60 | _easycom_v_status_bar(); 61 | } 62 | const __default__ = common_vendor.defineComponent({ 63 | name: "ComplexLayout", 64 | options: { 65 | virtualHost: true 66 | } 67 | }); 68 | const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent(__spreadProps(__spreadValues({}, __default__), { 69 | setup(__props) { 70 | return (_ctx, _cache) => { 71 | return { 72 | a: common_vendor.p({ 73 | ["container-class"]: "bg-[#fff]", 74 | ["container-style"]: { 75 | backgroundColor: "#fff" 76 | } 77 | }) 78 | }; 79 | }; 80 | } 81 | })); 82 | const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__file", "/Users/wurui/Documents/millcloud/guangda/miniapp/src/layouts/complex.vue"]]); 83 | wx.createComponent(Component); 84 | `), 85 | ).toBe("common_vendor"); 86 | expect( 87 | babelGetVendorName(`"use strict";var e=Object.defineProperty,t=Object.defineProperties,r=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o;const c=require("../common/vendor.js");if(!Array){(()=>"../components/v-status-bar/v-status-bar.js")()}Math;const p=c.defineComponent({name:"ComplexLayout",options:{virtualHost:!0}}),i=c.defineComponent((f=((e,t)=>{for(var r in t||(t={}))n.call(t,r)&&s(e,r,t[r]);if(o)for(var r of o(t))a.call(t,r)&&s(e,r,t[r]);return e})({},p),t(f,r({setup:e=>(e,t)=>({a:c.p({"container-class":"bg-[#fff]","container-style":{backgroundColor:"#fff"}})})}))));var f;wx.createComponent(i); 88 | `), 89 | ).toBe("c"); 90 | }); 91 | 92 | it("babelTransformScript", () => { 93 | expect( 94 | babelTransformScript( 95 | `"use strict"; 96 | var __defProp = Object.defineProperty; 97 | var __defProps = Object.defineProperties; 98 | var __getOwnPropDescs = Object.getOwnPropertyDescriptors; 99 | var __getOwnPropSymbols = Object.getOwnPropertySymbols; 100 | var __hasOwnProp = Object.prototype.hasOwnProperty; 101 | var __propIsEnum = Object.prototype.propertyIsEnumerable; 102 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 103 | var __spreadValues = (a, b) => { 104 | for (var prop in b || (b = {})) 105 | if (__hasOwnProp.call(b, prop)) 106 | __defNormalProp(a, prop, b[prop]); 107 | if (__getOwnPropSymbols) 108 | for (var prop of __getOwnPropSymbols(b)) { 109 | if (__propIsEnum.call(b, prop)) 110 | __defNormalProp(a, prop, b[prop]); 111 | } 112 | return a; 113 | }; 114 | var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); 115 | const common_vendor = require("../common/vendor.js"); 116 | const __unplugin_components_0 = () => "../components/v-status-bar/v-status-bar.js"; 117 | if (!Array) { 118 | const _easycom_v_status_bar2 = __unplugin_components_0; 119 | _easycom_v_status_bar2(); 120 | } 121 | const _easycom_v_status_bar = () => "../components/v-status-bar/v-status-bar.js"; 122 | if (!Math) { 123 | _easycom_v_status_bar(); 124 | } 125 | const __default__ = common_vendor.defineComponent({ 126 | name: "ComplexLayout", 127 | options: { 128 | virtualHost: true 129 | } 130 | }); 131 | const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent(__spreadProps(__spreadValues({}, __default__), { 132 | setup(__props) { 133 | return (_ctx, _cache) => { 134 | return { 135 | a: common_vendor.p({ 136 | ["container-class"]: "bg-[#fff]", 137 | ["test-class"]: { 138 | "bg-[#fff]": true, 139 | }, 140 | ["container-style"]: { 141 | backgroundColor: "#fff" 142 | } 143 | }) 144 | }; 145 | }; 146 | } 147 | })); 148 | const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__file", "/miniprogram/src/layouts/complex.vue"]]); 149 | wx.createComponent(Component); 150 | `, 151 | { renderProps: "p" }, 152 | ), 153 | ).toMatchInlineSnapshot(` 154 | ""use strict"; 155 | var __defProp=Object.defineProperty; 156 | var __defProps=Object.defineProperties; 157 | var __getOwnPropDescs=Object.getOwnPropertyDescriptors; 158 | var __getOwnPropSymbols=Object.getOwnPropertySymbols; 159 | var __hasOwnProp=Object.prototype.hasOwnProperty; 160 | var __propIsEnum=Object.prototype.propertyIsEnumerable; 161 | var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:true,configurable:true,writable:true,value}):obj[key]=value; 162 | var __spreadValues=(a,b)=>{ 163 | for(var prop in b||(b={})) 164 | if(__hasOwnProp.call(b,prop)) 165 | __defNormalProp(a,prop,b[prop]); 166 | if(__getOwnPropSymbols) 167 | for(var prop of __getOwnPropSymbols(b)){ 168 | if(__propIsEnum.call(b,prop)) 169 | __defNormalProp(a,prop,b[prop]); 170 | } 171 | return a; 172 | }; 173 | var __spreadProps=(a,b)=>__defProps(a,__getOwnPropDescs(b)); 174 | const common_vendor=require("../common/vendor.js"); 175 | const __unplugin_components_0=()=>"../components/v-status-bar/v-status-bar.js"; 176 | if(!Array){ 177 | const _easycom_v_status_bar2=__unplugin_components_0; 178 | _easycom_v_status_bar2(); 179 | } 180 | const _easycom_v_status_bar=()=>"../components/v-status-bar/v-status-bar.js"; 181 | if(!Math){ 182 | _easycom_v_status_bar(); 183 | } 184 | const __default__=common_vendor.defineComponent({ 185 | name:"ComplexLayout", 186 | options:{ 187 | virtualHost:true 188 | } 189 | }); 190 | const _sfc_main=/* @__PURE__ */common_vendor.defineComponent(__spreadProps(__spreadValues({},__default__),{ 191 | setup(__props){ 192 | return(_ctx,_cache)=>{ 193 | return{ 194 | a:common_vendor.p({ 195 | ["container-class"]:"bg---h-fff-", 196 | ["test-class"]:{ 197 | "bg---h-fff-":true 198 | }, 199 | ["container-style"]:{ 200 | backgroundColor:"#fff" 201 | } 202 | }) 203 | }; 204 | }; 205 | } 206 | })); 207 | const Component=/* @__PURE__ */common_vendor._export_sfc(_sfc_main,[["__file","/miniprogram/src/layouts/complex.vue"]]); 208 | wx.createComponent(Component);" 209 | `); 210 | expect( 211 | babelTransformScript( 212 | `"use strict";var e=Object.defineProperty,t=Object.defineProperties,r=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o;const c=require("../common/vendor.js");if(!Array){(()=>"../components/v-status-bar/v-status-bar.js")()}Math;const p=c.defineComponent({name:"ComplexLayout",options:{virtualHost:!0}}),i=c.defineComponent((f=((e,t)=>{for(var r in t||(t={}))n.call(t,r)&&s(e,r,t[r]);if(o)for(var r of o(t))a.call(t,r)&&s(e,r,t[r]);return e})({},p),t(f,r({setup:e=>(e,t)=>({a:c.p({"container-class":"bg-[#fff]","test-class":{"bg-[#fff]":true},"container-style":{backgroundColor:"#fff"}})})}))));var f;wx.createComponent(i);`, 213 | { renderProps: "p" }, 214 | ), 215 | ).toBe( 216 | `"use strict";var e=Object.defineProperty,t=Object.defineProperties,r=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o;const c=require("../common/vendor.js");if(!Array){(()=>"../components/v-status-bar/v-status-bar.js")();}Math;const p=c.defineComponent({name:"ComplexLayout",options:{virtualHost:!0}}),i=c.defineComponent((f=((e,t)=>{for(var r in t||(t={}))n.call(t,r)&&s(e,r,t[r]);if(o)for(var r of o(t))a.call(t,r)&&s(e,r,t[r]);return e;})({},p),t(f,r({setup:(e)=>(e,t)=>({a:c.p({"container-class":"bg---h-fff-","test-class":{"bg---h-fff-":true},"container-style":{backgroundColor:"#fff"}})})}))));var f;wx.createComponent(i);`, 217 | ); 218 | }); 219 | }); 220 | --------------------------------------------------------------------------------