Add style object support
This commit is contained in:
parent
835c5b7810
commit
398f2a7c69
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,4 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.runtime": "node"
|
||||
}
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@aet/tailwind",
|
||||
"version": "0.0.1-beta.20",
|
||||
"version": "0.0.1-beta.21",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@ -32,7 +32,7 @@
|
||||
"prettier": "^3.3.2",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tsup": "^8.1.0",
|
||||
"typescript": "^5.5.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.2",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
@ -43,9 +43,10 @@
|
||||
"@babel/core": "^7.24.7",
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-selector-parser": "^6.1.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"type-fest": "^4.20.1"
|
||||
"type-fest": "^4.21.0"
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
|
166
pnpm-lock.yaml
generated
166
pnpm-lock.yaml
generated
@ -18,18 +18,18 @@ importers:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
postcss:
|
||||
specifier: ^8.4.38
|
||||
version: 8.4.38
|
||||
specifier: ^8.4.39
|
||||
version: 8.4.39
|
||||
tiny-invariant:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
type-fest:
|
||||
specifier: ^4.20.1
|
||||
version: 4.20.1
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
devDependencies:
|
||||
'@aet/eslint-rules':
|
||||
specifier: ^0.0.34
|
||||
version: 0.0.34(eslint@8.57.0)(typescript@5.5.2)
|
||||
version: 0.0.34(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@types/babel__core':
|
||||
specifier: ^7.20.5
|
||||
version: 7.20.5
|
||||
@ -77,10 +77,13 @@ importers:
|
||||
version: 8.57.0
|
||||
postcss-nested:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(postcss@8.4.38)
|
||||
version: 6.0.1(postcss@8.4.39)
|
||||
postcss-safe-parser:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0(postcss@8.4.38)
|
||||
version: 7.0.0(postcss@8.4.39)
|
||||
postcss-selector-parser:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
prettier:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
@ -89,10 +92,10 @@ importers:
|
||||
version: 3.4.4
|
||||
tsup:
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0(postcss@8.4.38)(typescript@5.5.2)
|
||||
version: 8.1.0(postcss@8.4.39)(typescript@5.5.3)
|
||||
typescript:
|
||||
specifier: ^5.5.2
|
||||
version: 5.5.2
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
vite:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2(@types/node@20.14.9)
|
||||
@ -1630,6 +1633,9 @@ packages:
|
||||
picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
|
||||
picocolors@1.0.1:
|
||||
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@ -1685,15 +1691,15 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.4.31
|
||||
|
||||
postcss-selector-parser@6.0.16:
|
||||
resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
|
||||
postcss-selector-parser@6.1.0:
|
||||
resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.4.38:
|
||||
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
|
||||
postcss@8.4.39:
|
||||
resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
@ -2011,12 +2017,12 @@ packages:
|
||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
type-fest@4.20.1:
|
||||
resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==}
|
||||
type-fest@4.21.0:
|
||||
resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@5.5.2:
|
||||
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
|
||||
typescript@5.5.3:
|
||||
resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@ -2160,15 +2166,15 @@ snapshots:
|
||||
|
||||
'@aet/eslint-define-config@0.1.0-beta.1': {}
|
||||
|
||||
'@aet/eslint-rules@0.0.34(eslint@8.57.0)(typescript@5.5.2)':
|
||||
'@aet/eslint-rules@0.0.34(eslint@8.57.0)(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@aet/eslint-define-config': 0.1.0-beta.1
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@types/eslint': 8.56.10
|
||||
'@typescript-eslint/eslint-plugin': 7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/type-utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/eslint-plugin': 7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/type-utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
aria-query: 5.3.0
|
||||
axe-core: 4.9.1
|
||||
axobject-query: 4.0.0
|
||||
@ -2180,9 +2186,9 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-config-prettier: 9.1.0(eslint@8.57.0)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
|
||||
eslint-plugin-es-x: 7.7.0(eslint@8.57.0)
|
||||
eslint-plugin-import-x: 0.5.2(eslint@8.57.0)(typescript@5.5.2)
|
||||
eslint-plugin-import-x: 0.5.2(eslint@8.57.0)(typescript@5.5.3)
|
||||
eslint-plugin-jsdoc: 48.5.0(eslint@8.57.0)
|
||||
eslint-plugin-unicorn: 54.0.0(eslint@8.57.0)
|
||||
esprima: 4.0.1
|
||||
@ -2199,7 +2205,7 @@ snapshots:
|
||||
resolve: 2.0.0-next.5
|
||||
semver: 7.6.2
|
||||
tsconfig-paths: 4.2.0
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@ -2636,7 +2642,7 @@ snapshots:
|
||||
|
||||
'@types/postcss-safe-parser@5.0.4':
|
||||
dependencies:
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
|
||||
'@types/prop-types@15.7.12': {}
|
||||
|
||||
@ -2649,34 +2655,34 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.14.9
|
||||
|
||||
'@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/scope-manager': 7.14.1
|
||||
'@typescript-eslint/type-utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/type-utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
'@typescript-eslint/visitor-keys': 7.14.1
|
||||
eslint: 8.57.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.1
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 1.3.0(typescript@5.5.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.14.1
|
||||
'@typescript-eslint/types': 7.14.1
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.3)
|
||||
'@typescript-eslint/visitor-keys': 7.14.1
|
||||
debug: 4.3.5
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -2685,21 +2691,21 @@ snapshots:
|
||||
'@typescript-eslint/types': 7.14.1
|
||||
'@typescript-eslint/visitor-keys': 7.14.1
|
||||
|
||||
'@typescript-eslint/type-utils@7.14.1(eslint@8.57.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/type-utils@7.14.1(eslint@8.57.0)(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.3)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
debug: 4.3.5
|
||||
eslint: 8.57.0
|
||||
ts-api-utils: 1.3.0(typescript@5.5.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@7.14.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.2)':
|
||||
'@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.14.1
|
||||
'@typescript-eslint/visitor-keys': 7.14.1
|
||||
@ -2708,18 +2714,18 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.6.2
|
||||
ts-api-utils: 1.3.0(typescript@5.5.2)
|
||||
ts-api-utils: 1.3.0(typescript@5.5.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@7.14.1(eslint@8.57.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/utils@7.14.1(eslint@8.57.0)(typescript@5.5.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@typescript-eslint/scope-manager': 7.14.1
|
||||
'@typescript-eslint/types': 7.14.1
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 7.14.1(typescript@5.5.3)
|
||||
eslint: 8.57.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -3084,11 +3090,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.14.1(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
transitivePeerDependencies:
|
||||
@ -3101,9 +3107,9 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-compat-utils: 0.5.1(eslint@8.57.0)
|
||||
|
||||
eslint-plugin-import-x@0.5.2(eslint@8.57.0)(typescript@5.5.2):
|
||||
eslint-plugin-import-x@0.5.2(eslint@8.57.0)(typescript@5.5.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
|
||||
debug: 4.3.5
|
||||
doctrine: 3.0.0
|
||||
eslint: 8.57.0
|
||||
@ -3704,6 +3710,8 @@ snapshots:
|
||||
|
||||
picocolors@1.0.0: {}
|
||||
|
||||
picocolors@1.0.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
@ -3718,45 +3726,45 @@ snapshots:
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.4.38):
|
||||
postcss-import@15.1.0(postcss@8.4.39):
|
||||
dependencies:
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
postcss-value-parser: 4.2.0
|
||||
read-cache: 1.0.0
|
||||
resolve: 1.22.8
|
||||
|
||||
postcss-js@4.0.1(postcss@8.4.38):
|
||||
postcss-js@4.0.1(postcss@8.4.39):
|
||||
dependencies:
|
||||
camelcase-css: 2.0.1
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
|
||||
postcss-load-config@4.0.2(postcss@8.4.38):
|
||||
postcss-load-config@4.0.2(postcss@8.4.39):
|
||||
dependencies:
|
||||
lilconfig: 3.1.1
|
||||
yaml: 2.4.1
|
||||
optionalDependencies:
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
|
||||
postcss-nested@6.0.1(postcss@8.4.38):
|
||||
postcss-nested@6.0.1(postcss@8.4.39):
|
||||
dependencies:
|
||||
postcss: 8.4.38
|
||||
postcss-selector-parser: 6.0.16
|
||||
postcss: 8.4.39
|
||||
postcss-selector-parser: 6.1.0
|
||||
|
||||
postcss-safe-parser@7.0.0(postcss@8.4.38):
|
||||
postcss-safe-parser@7.0.0(postcss@8.4.39):
|
||||
dependencies:
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
|
||||
postcss-selector-parser@6.0.16:
|
||||
postcss-selector-parser@6.1.0:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.38:
|
||||
postcss@8.4.39:
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
picocolors: 1.0.1
|
||||
source-map-js: 1.2.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
@ -3981,12 +3989,12 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
object-hash: 3.0.0
|
||||
picocolors: 1.0.0
|
||||
postcss: 8.4.38
|
||||
postcss-import: 15.1.0(postcss@8.4.38)
|
||||
postcss-js: 4.0.1(postcss@8.4.38)
|
||||
postcss-load-config: 4.0.2(postcss@8.4.38)
|
||||
postcss-nested: 6.0.1(postcss@8.4.38)
|
||||
postcss-selector-parser: 6.0.16
|
||||
postcss: 8.4.39
|
||||
postcss-import: 15.1.0(postcss@8.4.39)
|
||||
postcss-js: 4.0.1(postcss@8.4.39)
|
||||
postcss-load-config: 4.0.2(postcss@8.4.39)
|
||||
postcss-nested: 6.0.1(postcss@8.4.39)
|
||||
postcss-selector-parser: 6.1.0
|
||||
resolve: 1.22.8
|
||||
sucrase: 3.35.0
|
||||
transitivePeerDependencies:
|
||||
@ -4024,9 +4032,9 @@ snapshots:
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-api-utils@1.3.0(typescript@5.5.2):
|
||||
ts-api-utils@1.3.0(typescript@5.5.3):
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 5.5.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
@ -4038,7 +4046,7 @@ snapshots:
|
||||
|
||||
tslib@2.6.3: {}
|
||||
|
||||
tsup@8.1.0(postcss@8.4.38)(typescript@5.5.2):
|
||||
tsup@8.1.0(postcss@8.4.39)(typescript@5.5.3):
|
||||
dependencies:
|
||||
bundle-require: 4.0.2(esbuild@0.21.5)
|
||||
cac: 6.7.14
|
||||
@ -4048,15 +4056,15 @@ snapshots:
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
joycon: 3.1.1
|
||||
postcss-load-config: 4.0.2(postcss@8.4.38)
|
||||
postcss-load-config: 4.0.2(postcss@8.4.39)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.13.2
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.35.0
|
||||
tree-kill: 1.2.2
|
||||
optionalDependencies:
|
||||
postcss: 8.4.38
|
||||
typescript: 5.5.2
|
||||
postcss: 8.4.39
|
||||
typescript: 5.5.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
@ -4073,9 +4081,9 @@ snapshots:
|
||||
|
||||
type-fest@0.8.1: {}
|
||||
|
||||
type-fest@4.20.1: {}
|
||||
type-fest@4.21.0: {}
|
||||
|
||||
typescript@5.5.2: {}
|
||||
typescript@5.5.3: {}
|
||||
|
||||
ufo@1.5.3: {}
|
||||
|
||||
@ -4118,7 +4126,7 @@ snapshots:
|
||||
vite@5.3.2(@types/node@20.14.9):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.4.38
|
||||
postcss: 8.4.39
|
||||
rollup: 4.13.2
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.9
|
||||
|
@ -28,6 +28,7 @@ await Promise.all([
|
||||
build({
|
||||
...tsupConfig,
|
||||
entry: ["src/index.ts"],
|
||||
external: ["postcss-selector-parser", "postcss"],
|
||||
}),
|
||||
Bun.write(
|
||||
"dist/package.json",
|
||||
|
25
src/__tests__/base.test.ts
Normal file
25
src/__tests__/base.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPostCSS } from "../index";
|
||||
import { getBuild, minCSS, name } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
const compileESBuild = getBuild("base");
|
||||
|
||||
it("supports importing tailwind/base", async () => {
|
||||
const postcss = createPostCSS({
|
||||
tailwindConfig: {},
|
||||
postCSSPlugins: [],
|
||||
});
|
||||
const base = await postcss("@tailwind base;");
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
import "${name}/base";
|
||||
`,
|
||||
});
|
||||
|
||||
expect(files.js.text).toBe("");
|
||||
expect(minCSS(files.css.text)).toContain(minCSS(base));
|
||||
});
|
||||
});
|
36
src/__tests__/error.test.ts
Normal file
36
src/__tests__/error.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getBuild } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
const compileESBuild = getBuild("error");
|
||||
it("reports errors with correct position", async () => {
|
||||
try {
|
||||
await compileESBuild({
|
||||
clsx: "emotion",
|
||||
jsxAttributeAction: "preserve",
|
||||
esbuild: {
|
||||
logLevel: "silent",
|
||||
},
|
||||
javascript: /* tsx */ `
|
||||
export function Hello() {
|
||||
return (
|
||||
<div css="text-center2 m-0">
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
throw new Error("Expected an error");
|
||||
} catch (e) {
|
||||
expect(e.errors).toHaveLength(1);
|
||||
const [error] = e.errors;
|
||||
expect(error.location).toMatchObject({
|
||||
column: 14,
|
||||
length: 12,
|
||||
line: 3,
|
||||
lineText: ' <div css="text-center2 m-0">',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
29
src/__tests__/styleObject.test.ts
Normal file
29
src/__tests__/styleObject.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getBuild } from "./utils";
|
||||
import { getClassName } from "../index";
|
||||
|
||||
describe.only("babel-tailwind", () => {
|
||||
const compileESBuild = getBuild("styleObject");
|
||||
|
||||
it("supports conversion into CSSProperties", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 1,
|
||||
javascript: `
|
||||
export function Hello() {
|
||||
return (
|
||||
<div style={tws\`p-2 text-center\`}>
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const clsName = getClassName("p-2 text-center").replace(/^tw-/, "tw_");
|
||||
expect(files.js.text).toContain(
|
||||
`var ${clsName} = {\n "padding": "0.5rem",\n "textAlign": "center"\n}`
|
||||
);
|
||||
expect(files.js.text).toContain(`style: ${clsName}`);
|
||||
});
|
||||
});
|
51
src/__tests__/tw.test.ts
Normal file
51
src/__tests__/tw.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getClassName } from "../index";
|
||||
import { getBuild } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
const compileESBuild = getBuild("tw");
|
||||
it("supports grouped tw", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: `
|
||||
export default tw("text-sm", \`flex\`, {
|
||||
"group-hover": "text-center",
|
||||
"[&>div]": \`font-semibold\`,
|
||||
data: {
|
||||
"name='hello'": "text-right",
|
||||
nested: {
|
||||
true: "border",
|
||||
}
|
||||
},
|
||||
})
|
||||
`,
|
||||
});
|
||||
|
||||
const clsName = getClassName(
|
||||
"text-sm flex group-hover:text-center [&>div]:font-semibold data-[name='hello']:text-right data-[nested=true]:border"
|
||||
);
|
||||
expect(files.js.text).toContain(`= "${clsName}"`);
|
||||
expect(files.css.text).toMatch(
|
||||
[
|
||||
`.${clsName} {`,
|
||||
" display: flex;",
|
||||
" font-size: 0.875rem;",
|
||||
" line-height: 1.25rem;",
|
||||
"}",
|
||||
`.group:hover .${clsName} {`,
|
||||
" text-align: center;",
|
||||
"}",
|
||||
`.${clsName}[data-nested=true] {`,
|
||||
" border-width: 1px;",
|
||||
"}",
|
||||
`.${clsName}[data-name=hello] {`,
|
||||
" text-align: right;",
|
||||
"}",
|
||||
`.${clsName} > div {`,
|
||||
" font-weight: 600;",
|
||||
"}",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
});
|
75
src/__tests__/utils.ts
Normal file
75
src/__tests__/utils.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { afterEach, beforeEach, expect } from "vitest";
|
||||
import * as esbuild from "esbuild";
|
||||
import dedent from "dedent";
|
||||
import { type TailwindPluginOptions, babelPlugin, getTailwindPlugins } from "../index";
|
||||
|
||||
export { name } from "../../package.json" with { type: "json" };
|
||||
|
||||
export const minCSS = (text: string) =>
|
||||
esbuild.transformSync(text, { minify: true, loader: "css" }).code;
|
||||
|
||||
const findByExt = (outputFiles: esbuild.OutputFile[], ext: string) =>
|
||||
outputFiles.find(file => file.path.endsWith(ext))!;
|
||||
|
||||
export function getBuild(name: string) {
|
||||
const folder = resolve(import.meta.dirname, "..", ".temp-" + name);
|
||||
|
||||
async function write(path: string, content: string) {
|
||||
const resolved = resolve(folder, path);
|
||||
await fs.writeFile(resolved, content);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(folder, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
return async function compileESBuild({
|
||||
javascript,
|
||||
esbuild: esbuildOptions,
|
||||
expectFiles,
|
||||
...options
|
||||
}: Omit<TailwindPluginOptions, "compile"> & {
|
||||
esbuild?: esbuild.BuildOptions;
|
||||
javascript: string;
|
||||
expectFiles?: number;
|
||||
}) {
|
||||
const tailwind = getTailwindPlugins({
|
||||
tailwindConfig: {},
|
||||
macroFunction: "tw",
|
||||
macroStyleFunction: "tws",
|
||||
...options,
|
||||
});
|
||||
const result = await esbuild.build({
|
||||
bundle: true,
|
||||
write: false,
|
||||
external: ["react/jsx-runtime", "@emotion/css", "clsx"],
|
||||
outdir: "dist",
|
||||
format: "esm",
|
||||
entryPoints: [await write("index.tsx", dedent(javascript))],
|
||||
plugins: [babelPlugin({ plugins: [tailwind.babel()] }), tailwind.esbuild()],
|
||||
...esbuildOptions,
|
||||
});
|
||||
|
||||
const { errors, warnings, outputFiles } = result;
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(warnings).toHaveLength(0);
|
||||
|
||||
if (expectFiles != null) {
|
||||
expect(outputFiles).toHaveLength(expectFiles);
|
||||
}
|
||||
|
||||
return {
|
||||
outputFiles: outputFiles!,
|
||||
files: new Proxy({} as Record<string, esbuild.OutputFile>, {
|
||||
get: (_, ext: string) => findByExt(outputFiles!, ext),
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
@ -7,46 +7,8 @@ import { type NodePath, type types as t } from "@babel/core";
|
||||
import type { SourceLocation, StyleMapEntry } from "./shared";
|
||||
import { type ResolveTailwindOptions, getClassName } from "./index";
|
||||
|
||||
const definePlugin =
|
||||
<T>(fn: (runtime: typeof babel) => babel.Visitor<babel.PluginPass & T>) =>
|
||||
(runtime: typeof babel) => {
|
||||
const plugin: babel.PluginObj<babel.PluginPass & T> = {
|
||||
visitor: fn(runtime),
|
||||
};
|
||||
return plugin as babel.PluginObj;
|
||||
};
|
||||
|
||||
const extractJSXContainer = (attr: NonNullable<t.JSXAttribute["value"]>): t.Expression =>
|
||||
attr.type === "JSXExpressionContainer" ? (attr.expression as t.Expression) : attr;
|
||||
|
||||
function matchPath(
|
||||
nodePath: NodePath<t.Node | null | undefined>,
|
||||
fns: (dig: (nodePath: NodePath<t.Node | null | undefined>) => void) => babel.Visitor
|
||||
) {
|
||||
if (!nodePath.node) return;
|
||||
const fn = fns(path => matchPath(path, fns))[nodePath.node.type] as any;
|
||||
fn?.(nodePath);
|
||||
}
|
||||
|
||||
interface BabelPluginState {
|
||||
getCx: () => t.Identifier;
|
||||
sliceText: (node: t.Node) => SourceLocation;
|
||||
recordIfAbsent: (node: StyleMapEntry) => void;
|
||||
tailwindMap: Map<string, StyleMapEntry>;
|
||||
}
|
||||
|
||||
export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
|
||||
|
||||
const trim = (value: string) => value.replace(/\s+/g, " ").trim();
|
||||
const trimPrefix = (cls: string, prefix: string) =>
|
||||
trim(cls)
|
||||
.split(" ")
|
||||
.map(value => prefix + value);
|
||||
|
||||
const flatMapEntries = <K extends string | number, V, R>(
|
||||
map: Record<K, V>,
|
||||
fn: (value: V, key: K) => R[]
|
||||
): R[] => Object.entries(map).flatMap(([key, value]) => fn(value as V, key as K));
|
||||
type Type = "css" | "js";
|
||||
|
||||
export function babelTailwind(
|
||||
{
|
||||
@ -54,168 +16,172 @@ export function babelTailwind(
|
||||
clsx,
|
||||
getClassName: getClass = getClassName,
|
||||
macroFunction,
|
||||
macroStyleFunction,
|
||||
jsxAttributeAction = "delete",
|
||||
jsxAttributeName = "css",
|
||||
vite,
|
||||
}: ResolveTailwindOptions,
|
||||
onCollect: ClassNameCollector | undefined
|
||||
) {
|
||||
function getClsxImport(t: typeof babel.types, cx: t.Identifier) {
|
||||
switch (clsx) {
|
||||
case "emotion":
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("cx"))],
|
||||
t.stringLiteral("@emotion/css")
|
||||
);
|
||||
case "clsx":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
t.stringLiteral("clsx")
|
||||
);
|
||||
case "classnames":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
t.stringLiteral("classnames")
|
||||
);
|
||||
default:
|
||||
throw new Error("Unknown clsx library");
|
||||
type BabelPluginState = ReturnType<typeof getState>;
|
||||
|
||||
function getState(
|
||||
path: NodePath<t.Program>,
|
||||
state: babel.PluginPass,
|
||||
t: typeof babel.types
|
||||
) {
|
||||
let cx: t.Identifier;
|
||||
let styleImport: t.Identifier;
|
||||
|
||||
const cssMap = new Map<string, StyleMapEntry>();
|
||||
const jsMap = new Map<string, StyleMapEntry>();
|
||||
|
||||
function getStyleImport() {
|
||||
styleImport ??= path.scope.generateUidIdentifier("styles");
|
||||
return t.cloneNode(styleImport);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateArgs(paths: NodePath[]) {
|
||||
return paths
|
||||
.flatMap(path => {
|
||||
const { confident, value } = path.evaluate();
|
||||
invariant(confident, `${macroFunction} argument cannot be statically evaluated`);
|
||||
return {
|
||||
getClass(type: Type, value: string) {
|
||||
return type === "css" ? getClass(value) : "tw_" + hash(value);
|
||||
},
|
||||
|
||||
if (typeof value === "string") {
|
||||
return trim(value);
|
||||
sliceText: (node: t.Node): SourceLocation => ({
|
||||
filename: state.filename!,
|
||||
start: node.loc!.start,
|
||||
end: node.loc!.end,
|
||||
text: state.file.code
|
||||
.split("\n")
|
||||
.slice(node.loc!.start.line - 1, node.loc!.end.line)
|
||||
.join("\n"),
|
||||
}),
|
||||
|
||||
recordIfAbsent(type: Type, entry: StyleMapEntry) {
|
||||
const map = type === "css" ? cssMap : jsMap;
|
||||
if (!map.has(entry.key)) {
|
||||
map.set(entry.key, entry);
|
||||
}
|
||||
},
|
||||
|
||||
replaceWithImport(type: Type, path: NodePath, className: string) {
|
||||
if (type === "css") {
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
} else {
|
||||
const styleImportId = getStyleImport();
|
||||
path.replaceWith(
|
||||
t.memberExpression(styleImportId, t.stringLiteral(className), true)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getCx: () => {
|
||||
if (cx == null) {
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
path.node.body.unshift(getClsxImport(t, cx, clsx));
|
||||
}
|
||||
return t.cloneNode(cx);
|
||||
},
|
||||
|
||||
finish(node: t.Program) {
|
||||
const { filename } = state;
|
||||
if (!cssMap.size && !jsMap.size) return;
|
||||
invariant(filename, "babel: missing state.filename");
|
||||
|
||||
if (cssMap.size) {
|
||||
const cssName = basename(filename, extname(filename)) + ".css";
|
||||
const path = join(dirname(filename), cssName);
|
||||
const value = Array.from(cssMap.values());
|
||||
const importee = `tailwind:./${cssName}` + getSuffix(vite, value);
|
||||
|
||||
node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
|
||||
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return flatMapEntries(value, (classes, modifier) => {
|
||||
if (modifier === "data" && isPlainObject(classes)) {
|
||||
return flatMapEntries(
|
||||
classes as Record<string, string | object>,
|
||||
(cls, key) =>
|
||||
typeof cls === "string"
|
||||
? trimPrefix(cls, `${modifier}-[${key}]:`)
|
||||
: flatMapEntries(cls as Record<string, string>, (cls, attrValue) =>
|
||||
trimPrefix(cls, `${modifier}-[${key}=${attrValue}]:`)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (jsMap.size) {
|
||||
const jsName = basename(filename, extname(filename)) + ".tailwindStyle.js";
|
||||
const path = join(dirname(filename), jsName);
|
||||
const value = Array.from(jsMap.values());
|
||||
const importee = `tailwind:./${jsName}` + getSuffix(vite, value);
|
||||
|
||||
invariant(
|
||||
typeof classes === "string",
|
||||
`Value for "${modifier}" should be a string`
|
||||
);
|
||||
return trimPrefix(classes, modifier + ":");
|
||||
});
|
||||
node.body.unshift(
|
||||
t.importDeclaration(
|
||||
[t.importNamespaceSpecifier(getStyleImport())],
|
||||
t.stringLiteral(importee)
|
||||
)
|
||||
);
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
|
||||
throw new Error(`${macroFunction} argument has an invalid type`);
|
||||
})
|
||||
.join(" ");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const getType = (fnName: string): Type | undefined =>
|
||||
fnName === macroFunction ? "css" : fnName === macroStyleFunction ? "js" : undefined;
|
||||
|
||||
return definePlugin<BabelPluginState>(({ types: t }) => ({
|
||||
Program: {
|
||||
enter(path, state) {
|
||||
let cx: t.Identifier;
|
||||
const map = (state.tailwindMap = new Map());
|
||||
state.sliceText = node => ({
|
||||
filename: state.filename!,
|
||||
start: node.loc!.start,
|
||||
end: node.loc!.end,
|
||||
text: state.file.code
|
||||
.split("\n")
|
||||
.slice(node.loc!.start.line - 1, node.loc!.end.line)
|
||||
.join("\n"),
|
||||
});
|
||||
state.recordIfAbsent = entry => {
|
||||
if (!map.has(entry.key)) {
|
||||
map.set(entry.key, entry);
|
||||
}
|
||||
};
|
||||
state.getCx = () => {
|
||||
if (cx == null) {
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
path.node.body.unshift(getClsxImport(t, cx));
|
||||
}
|
||||
return t.cloneNode(cx);
|
||||
};
|
||||
Object.assign(state, getState(path, state, t));
|
||||
},
|
||||
|
||||
exit({ node }, { filename, tailwindMap }) {
|
||||
if (!tailwindMap.size) return;
|
||||
invariant(filename, "babel: missing state.filename");
|
||||
|
||||
const cssName = basename(filename, extname(filename)) + ".css";
|
||||
|
||||
const path = join(dirname(filename), cssName);
|
||||
const value = Array.from(tailwindMap.values());
|
||||
|
||||
let importee = `tailwind:./${cssName}`;
|
||||
if (vite) {
|
||||
const cacheKey = hash(value.map(x => x.className).join(","));
|
||||
importee += `?${cacheKey}`;
|
||||
}
|
||||
|
||||
node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
|
||||
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
exit({ node }, _) {
|
||||
_.finish(node);
|
||||
},
|
||||
},
|
||||
|
||||
TaggedTemplateExpression(path, { sliceText, recordIfAbsent }) {
|
||||
if (macroFunction == null) return;
|
||||
TaggedTemplateExpression(path, _) {
|
||||
if (macroFunction == null && macroStyleFunction == null) return;
|
||||
const { node } = path;
|
||||
|
||||
const {
|
||||
tag,
|
||||
quasi: { quasis, expressions },
|
||||
} = node;
|
||||
if (!t.isIdentifier(tag, { name: macroFunction })) return;
|
||||
if (!t.isIdentifier(tag)) return;
|
||||
|
||||
invariant(
|
||||
!expressions.length,
|
||||
`${macroFunction}\`\` should not contain expressions`
|
||||
);
|
||||
|
||||
const type = getType(tag.name);
|
||||
if (!type) return;
|
||||
|
||||
const value = quasis[0].value.cooked;
|
||||
if (value) {
|
||||
const trimmed = trim(value);
|
||||
const className = getClass(trimmed);
|
||||
recordIfAbsent({
|
||||
const className = _.getClass(type, trimmed);
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
className: trimmed,
|
||||
location: sliceText(node),
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
_.replaceWithImport(type, path, className);
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(path, { sliceText, recordIfAbsent }) {
|
||||
CallExpression(path, _) {
|
||||
if (macroFunction == null) return;
|
||||
|
||||
const { node } = path;
|
||||
|
||||
const { callee } = node;
|
||||
if (!t.isIdentifier(callee, { name: macroFunction })) return;
|
||||
if (!t.isIdentifier(callee)) return;
|
||||
|
||||
const trimmed = evaluateArgs(path.get("arguments"));
|
||||
const type = getType(callee.name);
|
||||
if (!type) return;
|
||||
|
||||
const trimmed = path.get("arguments").flatMap(evaluateArgs).join(" ");
|
||||
const className = getClass(trimmed);
|
||||
recordIfAbsent({
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
className: trimmed,
|
||||
location: sliceText(node),
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
_.replaceWithImport(type, path, className);
|
||||
},
|
||||
|
||||
JSXAttribute(path, { sliceText, recordIfAbsent, getCx }) {
|
||||
JSXAttribute(path, _) {
|
||||
const { name } = path.node;
|
||||
if (name.name !== jsxAttributeName) return;
|
||||
|
||||
@ -238,10 +204,10 @@ export function babelTailwind(
|
||||
const trimmed = trim(value);
|
||||
if (trimmed) {
|
||||
const className = getClass(trimmed);
|
||||
recordIfAbsent({
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
className: trimmed,
|
||||
location: sliceText(node),
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
}
|
||||
@ -252,12 +218,12 @@ export function babelTailwind(
|
||||
}
|
||||
},
|
||||
ObjectExpression(path) {
|
||||
const trimmed = evaluateArgs([path]);
|
||||
const trimmed = evaluateArgs(path).join(" ");
|
||||
const className = getClass(trimmed);
|
||||
recordIfAbsent({
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
className: trimmed,
|
||||
location: sliceText(path.node),
|
||||
location: _.sliceText(path.node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
},
|
||||
@ -292,7 +258,7 @@ export function babelTailwind(
|
||||
if (classNameAttribute) {
|
||||
const attrValue = classNameAttribute.value!;
|
||||
const wrap = (originalValue: babel.types.Expression) =>
|
||||
t.callExpression(getCx(), [originalValue, valuePathNode]);
|
||||
t.callExpression(_.getCx(), [originalValue, valuePathNode]);
|
||||
|
||||
// If both are string literals, we can merge them directly here
|
||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||
@ -313,7 +279,7 @@ export function babelTailwind(
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("className"),
|
||||
t.jSXExpressionContainer(valuePathNode!)
|
||||
t.jSXExpressionContainer(valuePathNode)
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -329,3 +295,92 @@ export function babelTailwind(
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function getClsxImport(t: typeof babel.types, cx: t.Identifier, clsx: string) {
|
||||
switch (clsx) {
|
||||
case "emotion":
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("cx"))],
|
||||
t.stringLiteral("@emotion/css")
|
||||
);
|
||||
case "clsx":
|
||||
return t.importDeclaration([t.importDefaultSpecifier(cx)], t.stringLiteral("clsx"));
|
||||
case "classnames":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
t.stringLiteral("classnames")
|
||||
);
|
||||
default:
|
||||
throw new Error("Unknown clsx library");
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateArgs(path: NodePath) {
|
||||
const { confident, value } = path.evaluate();
|
||||
invariant(confident, "Argument cannot be statically evaluated");
|
||||
|
||||
if (typeof value === "string") {
|
||||
return [trim(value)];
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return flatMapEntries(value, (classes, modifier) => {
|
||||
if (modifier === "data" && isPlainObject(classes)) {
|
||||
return flatMapEntries(classes as Record<string, string | object>, (cls, key) =>
|
||||
typeof cls === "string"
|
||||
? trimPrefix(cls, `${modifier}-[${key}]:`)
|
||||
: flatMapEntries(cls as Record<string, string>, (cls, attrValue) =>
|
||||
trimPrefix(cls, `${modifier}-[${key}=${attrValue}]:`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
typeof classes === "string",
|
||||
`Value for "${modifier}" should be a string`
|
||||
);
|
||||
return trimPrefix(classes, modifier + ":");
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Invalid argument type");
|
||||
}
|
||||
|
||||
const definePlugin =
|
||||
<T>(fn: (runtime: typeof babel) => babel.Visitor<babel.PluginPass & T>) =>
|
||||
(runtime: typeof babel) => {
|
||||
const plugin: babel.PluginObj<babel.PluginPass & T> = {
|
||||
visitor: fn(runtime),
|
||||
};
|
||||
return plugin as babel.PluginObj;
|
||||
};
|
||||
|
||||
const extractJSXContainer = (attr: NonNullable<t.JSXAttribute["value"]>): t.Expression =>
|
||||
attr.type === "JSXExpressionContainer" ? (attr.expression as t.Expression) : attr;
|
||||
|
||||
function matchPath(
|
||||
nodePath: NodePath<t.Node | null | undefined>,
|
||||
fns: (dig: (nodePath: NodePath<t.Node | null | undefined>) => void) => babel.Visitor
|
||||
) {
|
||||
if (!nodePath.node) return;
|
||||
const fn = fns(path => matchPath(path, fns))[nodePath.node.type] as any;
|
||||
fn?.(nodePath);
|
||||
}
|
||||
|
||||
function getSuffix(add: boolean | undefined, entries: StyleMapEntry[]) {
|
||||
if (!add) return "";
|
||||
|
||||
const cacheKey = hash(entries.map(x => x.className).join(","));
|
||||
return `?${cacheKey}`;
|
||||
}
|
||||
|
||||
const trim = (value: string) => value.replace(/\s+/g, " ").trim();
|
||||
const trimPrefix = (cls: string, prefix: string) =>
|
||||
trim(cls)
|
||||
.split(" ")
|
||||
.map(value => prefix + value);
|
||||
|
||||
const flatMapEntries = <K extends string | number, V, R>(
|
||||
map: Record<K, V>,
|
||||
fn: (value: V, key: K) => R[]
|
||||
): R[] => Object.entries(map).flatMap(([key, value]) => fn(value as V, key as K));
|
||||
|
289
src/css-to-js.ts
Executable file
289
src/css-to-js.ts
Executable file
@ -0,0 +1,289 @@
|
||||
// MIT License. Copyright (c) 2017 Brice BERNARD
|
||||
// https://github.com/brikou/CSS-in-JS-generator/commit/2a887d0d96f1d5044039d0e0457001f0fde0def0
|
||||
import {
|
||||
type AtRule,
|
||||
type Builder,
|
||||
type ChildNode,
|
||||
type Node,
|
||||
type Root,
|
||||
type Rule,
|
||||
parse,
|
||||
} from "postcss";
|
||||
import parseSelector from "postcss-selector-parser";
|
||||
import Stringifier from "postcss/lib/stringifier";
|
||||
import { camelCase } from "lodash";
|
||||
|
||||
function getSelectorScope(selector: string): string {
|
||||
let selectorScope = "root";
|
||||
|
||||
parseSelector(nodes => {
|
||||
for (const node of nodes.first.nodes) {
|
||||
if (node.type === "class") {
|
||||
selectorScope = node.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).processSync(selector);
|
||||
|
||||
return selectorScope;
|
||||
}
|
||||
|
||||
function getRequiredScopes(css: string, scope: string, knownScopes: Set<string>) {
|
||||
const requiredScopes = new Set<string>();
|
||||
|
||||
parse(css).walkRules(rule => {
|
||||
parseSelector(nodes => {
|
||||
nodes.walkClasses(node => {
|
||||
const selectorScope = getSelectorScope(node.toString());
|
||||
if (selectorScope === scope) return;
|
||||
|
||||
if (knownScopes.has(selectorScope)) {
|
||||
requiredScopes.add(selectorScope);
|
||||
}
|
||||
});
|
||||
}).processSync(rule.selector);
|
||||
});
|
||||
|
||||
return requiredScopes;
|
||||
}
|
||||
|
||||
function isNode(node: Node | undefined, type: "rule"): node is Rule;
|
||||
function isNode(node: Node | undefined, type: "atrule"): node is AtRule;
|
||||
function isNode(node: Node | undefined, type: "root"): node is Root;
|
||||
function isNode(node: Node | undefined, type: string): boolean {
|
||||
return node?.type === type;
|
||||
}
|
||||
|
||||
function addAll<T>(set: Set<T>, values: Iterable<T>) {
|
||||
for (const value of values) {
|
||||
set.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeScopes(node: Node) {
|
||||
const nodeScopes = new Set<string>();
|
||||
|
||||
if (
|
||||
isNode(node, "rule") &&
|
||||
(!isNode(node.parent, "atrule") || !/keyframes/.test(node.parent.name))
|
||||
) {
|
||||
addAll(nodeScopes, node.selectors.map(getSelectorScope));
|
||||
} else if (isNode(node, "atrule") && !node.name.endsWith("keyframes")) {
|
||||
node.walkRules(rule => {
|
||||
addAll(nodeScopes, rule.selectors.map(getSelectorScope));
|
||||
});
|
||||
}
|
||||
|
||||
if (!nodeScopes.size) {
|
||||
nodeScopes.add("root");
|
||||
}
|
||||
|
||||
return nodeScopes;
|
||||
}
|
||||
|
||||
function stringify(css: string, builder: Builder): void {
|
||||
new Stringifier(builder).stringify(parse(css));
|
||||
}
|
||||
|
||||
function getCssIndexedByScope(css: string) {
|
||||
const cssIndexedByScope = new Map<string, string>();
|
||||
const scopesStack = [new Set(["root"])];
|
||||
|
||||
stringify(css, (output, node, flag) => {
|
||||
if (flag === "start" && node) {
|
||||
scopesStack.push(getNodeScopes(node));
|
||||
}
|
||||
|
||||
if (flag === "end") {
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
for (const scope of scopesStack.at(-1)!) {
|
||||
if (!cssIndexedByScope.has(scope)) {
|
||||
cssIndexedByScope.set(scope, "");
|
||||
}
|
||||
|
||||
if (
|
||||
flag === "start" &&
|
||||
isNode(node, "rule") &&
|
||||
(!isNode(node.parent, "atrule") || !node.parent.name.endsWith("keyframes"))
|
||||
) {
|
||||
output = `${node.selectors
|
||||
.filter(selector => getSelectorScope(selector) === scope)
|
||||
.join(", ")} {`;
|
||||
}
|
||||
|
||||
cssIndexedByScope.set(scope, cssIndexedByScope.get(scope) + output);
|
||||
}
|
||||
|
||||
if (flag === "end") {
|
||||
scopesStack.pop();
|
||||
}
|
||||
});
|
||||
|
||||
return cssIndexedByScope;
|
||||
}
|
||||
|
||||
function convertSelectorForEmotion(
|
||||
selector: string,
|
||||
scope: string,
|
||||
knownScopes: Set<string>,
|
||||
mapClassNames: (className: string) => string
|
||||
): string {
|
||||
return parseSelector(nodes => {
|
||||
nodes.first.walkClasses(node => {
|
||||
if (node.toString() === scope) {
|
||||
node.toString = () => "&";
|
||||
} else if (knownScopes.has(node.toString())) {
|
||||
node.toString = () => `.\${${mapClassNames(node.value)}}`;
|
||||
}
|
||||
});
|
||||
}).processSync(selector);
|
||||
}
|
||||
|
||||
const convertScopeToModuleName = (scope: string) =>
|
||||
camelCase(scope)
|
||||
.replace(/^(\d)/, "_$1")
|
||||
.replace(
|
||||
/^(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with)$/,
|
||||
"_$1"
|
||||
);
|
||||
|
||||
function convertScopedCssForEmotion(
|
||||
scopedCss: string,
|
||||
scope: string,
|
||||
knownScopes: Set<string>,
|
||||
mapClassNames: (className: string) => string
|
||||
): string {
|
||||
let scopedCssForEmotion = "";
|
||||
|
||||
stringify(scopedCss, (output, node, flag) => {
|
||||
if ((flag === "start" || flag === "end") && isNode(node, "rule")) {
|
||||
if (node.selector === scope) {
|
||||
if (isNode(node.parent, "root")) {
|
||||
return;
|
||||
} else if (flag === "start") {
|
||||
output = "& {";
|
||||
}
|
||||
} else if (flag === "start") {
|
||||
const selectors = new Set(
|
||||
node.selectors.map(selector =>
|
||||
convertSelectorForEmotion(selector, scope, knownScopes, mapClassNames)
|
||||
)
|
||||
);
|
||||
|
||||
// TODO remove join usage once https://github.com/prettier/prettier/issues/2883 is resolved
|
||||
output = `${[...selectors].join(", ")} {`;
|
||||
}
|
||||
}
|
||||
|
||||
scopedCssForEmotion += output;
|
||||
});
|
||||
|
||||
return scopedCssForEmotion.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
||||
}
|
||||
|
||||
export function convertCssToJS(
|
||||
css: string,
|
||||
mapClassNames: (className: string) => string = convertScopeToModuleName
|
||||
): string {
|
||||
let cssForEmotion = "";
|
||||
|
||||
const cssIndexedByScope = getCssIndexedByScope(css);
|
||||
|
||||
if (cssIndexedByScope.has("root") && cssIndexedByScope.get("root")!.trim()) {
|
||||
cssForEmotion += 'import { injectGlobal } from "@emotion/css";\n';
|
||||
}
|
||||
|
||||
const knownScopes = new Set(cssIndexedByScope.keys());
|
||||
|
||||
const collator = new Intl.Collator(undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
|
||||
const sortedKnownScopes = [...knownScopes]
|
||||
.sort((scopeA, scopeB) => (scopeA === "root" ? -1 : collator.compare(scopeA, scopeB)))
|
||||
.reduce((previousSortedKnownScopes, knownScope) => {
|
||||
for (const requiredScope of getRequiredScopes(
|
||||
cssIndexedByScope.get(knownScope)!,
|
||||
knownScope,
|
||||
knownScopes
|
||||
)) {
|
||||
previousSortedKnownScopes.add(requiredScope);
|
||||
}
|
||||
previousSortedKnownScopes.add(knownScope);
|
||||
return previousSortedKnownScopes;
|
||||
}, new Set<string>());
|
||||
|
||||
for (const scope of sortedKnownScopes) {
|
||||
cssForEmotion += "\n";
|
||||
|
||||
const convertedScopedCssForEmotion = convertScopedCssForEmotion(
|
||||
cssIndexedByScope.get(scope)!,
|
||||
scope,
|
||||
knownScopes,
|
||||
mapClassNames
|
||||
).trimEnd();
|
||||
|
||||
if (!convertedScopedCssForEmotion.trim()) continue;
|
||||
|
||||
cssForEmotion +=
|
||||
scope === "root"
|
||||
? `injectGlobal\`${convertedScopedCssForEmotion}\n\`;\n`
|
||||
: `\nexport const ${mapClassNames(
|
||||
scope
|
||||
)} = ${JSON.stringify(asJSObject(convertedScopedCssForEmotion), null, 2)};\n`;
|
||||
}
|
||||
|
||||
return cssForEmotion.trim();
|
||||
}
|
||||
|
||||
const candidates = new Set(["fontSize"]);
|
||||
function simplifyValue(propName: string, value: string) {
|
||||
const num = value.match(/^(\d+(\.\d+)?)px$/)?.[1];
|
||||
if (num != null && candidates.has(propName)) {
|
||||
return parseFloat(num);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function asJSObject(inputCssText: string) {
|
||||
const css = parse(`a{${inputCssText}}`);
|
||||
const result: Record<string, string> = {};
|
||||
if (css.nodes.length !== 1) {
|
||||
throw new Error("Expected exactly one root node");
|
||||
}
|
||||
|
||||
const node = css.first!;
|
||||
if (node.type !== "rule") return;
|
||||
|
||||
function walk(collect: Record<string, any>, node: ChildNode) {
|
||||
switch (node.type) {
|
||||
case "atrule":
|
||||
const obj = (collect[`@${node.name} ${node.params}`] ??= {});
|
||||
node.each(child => {
|
||||
walk(obj, child);
|
||||
});
|
||||
break;
|
||||
|
||||
case "decl":
|
||||
const propName = node.prop.replace(/(-.)/g, v => v[1].toUpperCase());
|
||||
collect[propName] = simplifyValue(propName, node.value);
|
||||
break;
|
||||
|
||||
case "rule":
|
||||
node.each(declaration => {
|
||||
walk(collect, declaration);
|
||||
});
|
||||
break;
|
||||
|
||||
case "comment":
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
walk(result, node);
|
||||
|
||||
return result as React.CSSProperties;
|
||||
}
|
@ -19,7 +19,7 @@ export const babelPlugin = ({
|
||||
}): esbuild.Plugin => ({
|
||||
name: "babel-plugin",
|
||||
setup(build) {
|
||||
build.onLoad({ filter }, ({ path }) => {
|
||||
build.onLoad({ filter, namespace: "file" }, ({ path }) => {
|
||||
const load = once(() => readFileSync(path, "utf-8"));
|
||||
const plugins = Array.isArray(getPlugins)
|
||||
? getPlugins
|
||||
|
@ -1,16 +1,25 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import type * as esbuild from "esbuild";
|
||||
import { CssSyntaxError } from "postcss";
|
||||
import { type Compile, type StyleMap, pkgName, toCSSText } from "./shared";
|
||||
import { type Compile, type StyleMap, pkgName } from "./shared";
|
||||
import type { BuildStyleFile } from "./index";
|
||||
|
||||
const PLUGIN_NAME = "tailwind";
|
||||
const ESBUILD_NAMESPACE = "babel-tailwind";
|
||||
|
||||
export const esbuildPlugin = (styleMap: StyleMap, compile: Compile): esbuild.Plugin => ({
|
||||
export const esbuildPlugin = ({
|
||||
styleMap,
|
||||
compile,
|
||||
buildStyleFile,
|
||||
}: {
|
||||
styleMap: StyleMap;
|
||||
compile: Compile;
|
||||
buildStyleFile: BuildStyleFile;
|
||||
}): esbuild.Plugin => ({
|
||||
name: PLUGIN_NAME,
|
||||
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^tailwind:.+\.css$/ }, ({ path, importer }) => {
|
||||
build.onResolve({ filter: /^tailwind:.+\.\w+$/ }, ({ path, importer }) => {
|
||||
const resolved = join(dirname(importer), path.replace(/^tailwind:/, ""));
|
||||
if (styleMap.has(resolved)) {
|
||||
return {
|
||||
@ -38,11 +47,8 @@ export const esbuildPlugin = (styleMap: StyleMap, compile: Compile): esbuild.Plu
|
||||
const styles = styleMap.get(path)!;
|
||||
|
||||
try {
|
||||
const result = await compile(toCSSText(styles));
|
||||
return {
|
||||
contents: result,
|
||||
loader: "css",
|
||||
};
|
||||
const [loader, contents] = await buildStyleFile(path);
|
||||
return { contents, loader };
|
||||
} catch (e) {
|
||||
if (e instanceof CssSyntaxError) {
|
||||
const lines = e.source!.split("\n");
|
||||
|
@ -1,28 +1,10 @@
|
||||
/* eslint-disable unicorn/string-content */
|
||||
import { promises as fs } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import * as esbuild from "esbuild";
|
||||
import dedent from "dedent";
|
||||
import { name } from "../package.json" with { type: "json" };
|
||||
import {
|
||||
type TailwindPluginOptions,
|
||||
babelPlugin,
|
||||
createPostCSS,
|
||||
getClassName,
|
||||
getTailwindPlugins,
|
||||
} from "./index";
|
||||
|
||||
const folder = resolve(import.meta.dirname, "temp");
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getClassName } from "./index";
|
||||
import { getBuild } from "./__tests__/utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(folder, { recursive: true, force: true });
|
||||
});
|
||||
const compileESBuild = getBuild("main");
|
||||
|
||||
it("supports ESBuild", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
@ -69,13 +51,13 @@ describe("babel-tailwind", () => {
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
export function Hello() {
|
||||
return (
|
||||
<div className="text-center" css="text-center">
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function Hello() {
|
||||
return (
|
||||
<div className="text-center" css="text-center">
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
@ -107,37 +89,6 @@ describe("babel-tailwind", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports errors with correct position", async () => {
|
||||
try {
|
||||
await compileESBuild({
|
||||
clsx: "emotion",
|
||||
jsxAttributeAction: "preserve",
|
||||
esbuild: {
|
||||
logLevel: "silent",
|
||||
},
|
||||
javascript: /* tsx */ `
|
||||
export function Hello() {
|
||||
return (
|
||||
<div css="text-center2 m-0">
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
throw new Error("Expected an error");
|
||||
} catch (e) {
|
||||
expect(e.errors).toHaveLength(1);
|
||||
const [error] = e.errors;
|
||||
expect(error.location).toMatchObject({
|
||||
column: 14,
|
||||
length: 12,
|
||||
line: 3,
|
||||
lineText: ' <div css="text-center2 m-0">',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("supports custom jsxAttributeName", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
@ -159,51 +110,6 @@ describe("babel-tailwind", () => {
|
||||
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
||||
});
|
||||
|
||||
it("supports grouped tw", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
export default tw("text-sm", \`flex\`, {
|
||||
"group-hover": "text-center",
|
||||
"[&>div]": \`font-semibold\`,
|
||||
data: {
|
||||
"name='hello'": "text-right",
|
||||
nested: {
|
||||
true: "border",
|
||||
}
|
||||
},
|
||||
})
|
||||
`,
|
||||
});
|
||||
|
||||
const clsName = getClassName(
|
||||
"text-sm flex group-hover:text-center [&>div]:font-semibold data-[name='hello']:text-right data-[nested=true]:border"
|
||||
);
|
||||
expect(files.js.text).toContain(`= "${clsName}"`);
|
||||
expect(files.css.text).toMatch(
|
||||
[
|
||||
`.${clsName} {`,
|
||||
" display: flex;",
|
||||
" font-size: 0.875rem;",
|
||||
" line-height: 1.25rem;",
|
||||
"}",
|
||||
`.group:hover .${clsName} {`,
|
||||
" text-align: center;",
|
||||
"}",
|
||||
`.${clsName}[data-nested=true] {`,
|
||||
" border-width: 1px;",
|
||||
"}",
|
||||
`.${clsName}[data-name=hello] {`,
|
||||
" text-align: right;",
|
||||
"}",
|
||||
`.${clsName} > div {`,
|
||||
" font-weight: 600;",
|
||||
"}",
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
it("supports grouped array css attribute", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
@ -252,76 +158,4 @@ describe("babel-tailwind", () => {
|
||||
expect(files.js.text).toContain(`className: isCenter ? "${clsName}" : void 0`);
|
||||
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
||||
});
|
||||
|
||||
it("supports importing tailwind/base", async () => {
|
||||
const postcss = createPostCSS({
|
||||
tailwindConfig: {},
|
||||
postCSSPlugins: [],
|
||||
});
|
||||
const base = await postcss("@tailwind base;");
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
import "${name}/base";
|
||||
`,
|
||||
});
|
||||
|
||||
expect(files.js.text).toBe("");
|
||||
expect(minCSS(files.css.text)).toContain(minCSS(base));
|
||||
});
|
||||
});
|
||||
|
||||
async function write(path: string, content: string) {
|
||||
const resolved = resolve(folder, path);
|
||||
await fs.writeFile(resolved, content);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const minCSS = (text: string) =>
|
||||
esbuild.transformSync(text, { minify: true, loader: "css" }).code;
|
||||
|
||||
const findByExt = (outputFiles: esbuild.OutputFile[], ext: string) =>
|
||||
outputFiles.find(file => file.path.endsWith(ext))!;
|
||||
|
||||
async function compileESBuild({
|
||||
javascript,
|
||||
esbuild: esbuildOptions,
|
||||
expectFiles,
|
||||
...options
|
||||
}: Omit<TailwindPluginOptions, "compile"> & {
|
||||
esbuild?: esbuild.BuildOptions;
|
||||
javascript: string;
|
||||
expectFiles?: number;
|
||||
}) {
|
||||
const tailwind = getTailwindPlugins({
|
||||
tailwindConfig: {},
|
||||
macroFunction: "tw",
|
||||
...options,
|
||||
});
|
||||
const result = await esbuild.build({
|
||||
bundle: true,
|
||||
write: false,
|
||||
external: ["react/jsx-runtime", "@emotion/css", "clsx"],
|
||||
outdir: "dist",
|
||||
format: "esm",
|
||||
entryPoints: [await write("index.tsx", dedent(javascript))],
|
||||
plugins: [babelPlugin({ plugins: [tailwind.babel()] }), tailwind.esbuild()],
|
||||
...esbuildOptions,
|
||||
});
|
||||
|
||||
const { errors, warnings, outputFiles } = result;
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(warnings).toHaveLength(0);
|
||||
|
||||
if (expectFiles != null) {
|
||||
expect(outputFiles).toHaveLength(expectFiles);
|
||||
}
|
||||
|
||||
return {
|
||||
outputFiles: outputFiles!,
|
||||
files: new Proxy({} as Record<string, esbuild.OutputFile>, {
|
||||
get: (_, ext: string) => findByExt(outputFiles!, ext),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
47
src/index.ts
47
src/index.ts
@ -7,11 +7,15 @@ import { type ClassNameCollector, babelTailwind } from "./babel-tailwind";
|
||||
import { esbuildPlugin } from "./esbuild-postcss";
|
||||
import { vitePlugin } from "./vite-plugin";
|
||||
import { type StyleMap, createPostCSS } from "./shared";
|
||||
import { convertCssToJS } from "./css-to-js";
|
||||
|
||||
export { babelPlugin } from "./esbuild-babel";
|
||||
export { createPostCSS } from "./shared";
|
||||
|
||||
type GetClassName = (className: string) => string;
|
||||
export type BuildStyleFile = (
|
||||
path: string
|
||||
) => Promise<readonly ["css", string] | readonly ["js", string]>;
|
||||
|
||||
interface RecursiveStringObject {
|
||||
[modifier: string]: string | RecursiveStringObject;
|
||||
@ -20,7 +24,7 @@ interface RecursiveStringObject {
|
||||
export type CSSAttributeValue = string | (string | RecursiveStringObject)[];
|
||||
|
||||
/**
|
||||
* Tagged template macro function for Tailwind classes
|
||||
* Tagged template macro function combining Tailwind classes
|
||||
* @example "tw" => tw`p-2 text-center`
|
||||
*/
|
||||
export interface TailwindFunction {
|
||||
@ -28,6 +32,15 @@ export interface TailwindFunction {
|
||||
(...args: (string | RecursiveStringObject)[]): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tagged template macro function compiling Tailwind styles
|
||||
* @example "tws" => tws`p-2 text-center` // { padding: 2, textAlign: "center" }
|
||||
*/
|
||||
export interface TailwindStyleFunction<Output = React.CSSProperties> {
|
||||
(strings: TemplateStringsArray): Output;
|
||||
(...args: (string | RecursiveStringObject)[]): Output;
|
||||
}
|
||||
|
||||
export interface TailwindPluginOptions {
|
||||
/**
|
||||
* Tailwind CSS configuration
|
||||
@ -62,14 +75,21 @@ export interface TailwindPluginOptions {
|
||||
jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
|
||||
|
||||
/**
|
||||
* Template macro function to use for Tailwind classes
|
||||
* @default "tw"
|
||||
* Template macro function to combine Tailwind classes
|
||||
* @example
|
||||
* declare const tw: TailwindFunction;
|
||||
* "tw" => tw`p-2 text-center`
|
||||
*/
|
||||
macroFunction?: string | undefined;
|
||||
|
||||
/**
|
||||
* Template macro function to compile Tailwind classes
|
||||
* @example
|
||||
* declare const tws: TailwindStyleFunction;
|
||||
* "tws" => tws`p-2 text-center` // { padding: 2, textAlign: "center" }
|
||||
*/
|
||||
macroStyleFunction?: string | undefined;
|
||||
|
||||
/**
|
||||
* The prefix to use for the generated class names.
|
||||
* @default className => `tw-${hash(className)}`
|
||||
@ -108,6 +128,7 @@ export type ResolveTailwindOptions = SetRequired<
|
||||
| "styleMap"
|
||||
| "tailwindConfig"
|
||||
| "macroFunction"
|
||||
| "macroStyleFunction"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -140,6 +161,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
postCSSPlugins: [],
|
||||
styleMap: new Map(),
|
||||
macroFunction: undefined,
|
||||
macroStyleFunction: undefined,
|
||||
tailwindConfig: {},
|
||||
...options,
|
||||
};
|
||||
@ -149,12 +171,27 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
const { styleMap } = resolvedOptions;
|
||||
const compile = options.compile ?? memoize(getCompiler());
|
||||
|
||||
const buildStyleFile: BuildStyleFile = async path => {
|
||||
const styles = styleMap.get(path)!;
|
||||
const compiled = await compile(
|
||||
styles.map(({ className, key }) => `.${key} {\n @apply ${className}\n}`).join("\n")
|
||||
);
|
||||
if (path.endsWith(".css")) {
|
||||
return ["css", compiled] as const;
|
||||
} else if (path.endsWith(".js")) {
|
||||
const js = convertCssToJS(compiled, x => x.slice(1));
|
||||
return ["js", js] as const;
|
||||
} else {
|
||||
throw new Error("Unknown file extension");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
compile,
|
||||
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
||||
esbuild: () => esbuildPlugin(styleMap, compile),
|
||||
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
||||
/** Requires `options.vite` to be `true`. */
|
||||
vite: () => vitePlugin(styleMap, compile),
|
||||
vite: () => vitePlugin({ styleMap, compile, buildStyleFile }),
|
||||
styleMap,
|
||||
options,
|
||||
getCompiler,
|
||||
|
@ -52,3 +52,9 @@ export function toCSSText(tailwindMap: StyleMapEntry[]) {
|
||||
.map(({ className, key }) => `.${key} {\n @apply ${className}\n}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function toJSText(tailwindMap: StyleMapEntry[]) {
|
||||
return tailwindMap
|
||||
.map(({ className, key }) => `"${key}": "${className}"`)
|
||||
.join(",\n ");
|
||||
}
|
||||
|
@ -1,10 +1,19 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import type * as vite from "vite";
|
||||
import { type Compile, type StyleMap, pkgName, toCSSText } from "./shared";
|
||||
import { type Compile, type StyleMap, pkgName } from "./shared";
|
||||
import type { BuildStyleFile } from "./index";
|
||||
|
||||
const ROLLUP_PREFIX = "\0tailwind:";
|
||||
|
||||
export const vitePlugin = (styleMap: StyleMap, compile: Compile): vite.Plugin => ({
|
||||
export const vitePlugin = ({
|
||||
styleMap,
|
||||
compile,
|
||||
buildStyleFile,
|
||||
}: {
|
||||
styleMap: StyleMap;
|
||||
compile: Compile;
|
||||
buildStyleFile: BuildStyleFile;
|
||||
}): vite.Plugin => ({
|
||||
name: "tailwind",
|
||||
resolveId(id, importer) {
|
||||
if (id === `${pkgName}/base`) {
|
||||
@ -35,7 +44,7 @@ export const vitePlugin = (styleMap: StyleMap, compile: Compile): vite.Plugin =>
|
||||
|
||||
const name = resolved.split("?")[0];
|
||||
if (styleMap.has(name)) {
|
||||
return await compile(toCSSText(styleMap.get(name)!));
|
||||
return (await buildStyleFile(name))[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user