Add style object support

This commit is contained in:
Alex 2024-07-02 21:09:10 -04:00
parent 835c5b7810
commit 398f2a7c69
17 changed files with 896 additions and 435 deletions

View File

@ -1,4 +1,3 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true
"eslint.runtime": "node"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@aet/tailwind", "name": "@aet/tailwind",
"version": "0.0.1-beta.20", "version": "0.0.1-beta.21",
"main": "dist/index.js", "main": "dist/index.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -32,7 +32,7 @@
"prettier": "^3.3.2", "prettier": "^3.3.2",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"tsup": "^8.1.0", "tsup": "^8.1.0",
"typescript": "^5.5.2", "typescript": "^5.5.3",
"vite": "^5.3.2", "vite": "^5.3.2",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },
@ -43,9 +43,10 @@
"@babel/core": "^7.24.7", "@babel/core": "^7.24.7",
"@emotion/hash": "^0.9.1", "@emotion/hash": "^0.9.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"postcss": "^8.4.38", "postcss": "^8.4.39",
"postcss-selector-parser": "^6.1.0",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"type-fest": "^4.20.1" "type-fest": "^4.21.0"
}, },
"prettier": { "prettier": {
"arrowParens": "avoid", "arrowParens": "avoid",

166
pnpm-lock.yaml generated
View File

@ -18,18 +18,18 @@ importers:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
postcss: postcss:
specifier: ^8.4.38 specifier: ^8.4.39
version: 8.4.38 version: 8.4.39
tiny-invariant: tiny-invariant:
specifier: ^1.3.3 specifier: ^1.3.3
version: 1.3.3 version: 1.3.3
type-fest: type-fest:
specifier: ^4.20.1 specifier: ^4.21.0
version: 4.20.1 version: 4.21.0
devDependencies: devDependencies:
'@aet/eslint-rules': '@aet/eslint-rules':
specifier: ^0.0.34 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': '@types/babel__core':
specifier: ^7.20.5 specifier: ^7.20.5
version: 7.20.5 version: 7.20.5
@ -77,10 +77,13 @@ importers:
version: 8.57.0 version: 8.57.0
postcss-nested: postcss-nested:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(postcss@8.4.38) version: 6.0.1(postcss@8.4.39)
postcss-safe-parser: postcss-safe-parser:
specifier: ^7.0.0 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: prettier:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2 version: 3.3.2
@ -89,10 +92,10 @@ importers:
version: 3.4.4 version: 3.4.4
tsup: tsup:
specifier: ^8.1.0 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: typescript:
specifier: ^5.5.2 specifier: ^5.5.3
version: 5.5.2 version: 5.5.3
vite: vite:
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.3.2(@types/node@20.14.9) version: 5.3.2(@types/node@20.14.9)
@ -1630,6 +1633,9 @@ packages:
picocolors@1.0.0: picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
picomatch@2.3.1: picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
@ -1685,15 +1691,15 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.4.31 postcss: ^8.4.31
postcss-selector-parser@6.0.16: postcss-selector-parser@6.1.0:
resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
postcss-value-parser@4.2.0: postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.4.38: postcss@8.4.39:
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1: prelude-ls@1.2.1:
@ -2011,12 +2017,12 @@ packages:
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
engines: {node: '>=8'} engines: {node: '>=8'}
type-fest@4.20.1: type-fest@4.21.0:
resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==} resolution: {integrity: sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==}
engines: {node: '>=16'} engines: {node: '>=16'}
typescript@5.5.2: typescript@5.5.3:
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
@ -2160,15 +2166,15 @@ snapshots:
'@aet/eslint-define-config@0.1.0-beta.1': {} '@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: dependencies:
'@aet/eslint-define-config': 0.1.0-beta.1 '@aet/eslint-define-config': 0.1.0-beta.1
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@types/eslint': 8.56.10 '@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/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.2) '@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.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.2) '@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
aria-query: 5.3.0 aria-query: 5.3.0
axe-core: 4.9.1 axe-core: 4.9.1
axobject-query: 4.0.0 axobject-query: 4.0.0
@ -2180,9 +2186,9 @@ snapshots:
eslint: 8.57.0 eslint: 8.57.0
eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-node: 0.3.9 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-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-jsdoc: 48.5.0(eslint@8.57.0)
eslint-plugin-unicorn: 54.0.0(eslint@8.57.0) eslint-plugin-unicorn: 54.0.0(eslint@8.57.0)
esprima: 4.0.1 esprima: 4.0.1
@ -2199,7 +2205,7 @@ snapshots:
resolve: 2.0.0-next.5 resolve: 2.0.0-next.5
semver: 7.6.2 semver: 7.6.2
tsconfig-paths: 4.2.0 tsconfig-paths: 4.2.0
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
@ -2636,7 +2642,7 @@ snapshots:
'@types/postcss-safe-parser@5.0.4': '@types/postcss-safe-parser@5.0.4':
dependencies: dependencies:
postcss: 8.4.38 postcss: 8.4.39
'@types/prop-types@15.7.12': {} '@types/prop-types@15.7.12': {}
@ -2649,34 +2655,34 @@ snapshots:
dependencies: dependencies:
'@types/node': 20.14.9 '@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: dependencies:
'@eslint-community/regexpp': 4.10.0 '@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/scope-manager': 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)
'@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)
'@typescript-eslint/visitor-keys': 7.14.1 '@typescript-eslint/visitor-keys': 7.14.1
eslint: 8.57.0 eslint: 8.57.0
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.1 ignore: 5.3.1
natural-compare: 1.4.0 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: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@typescript-eslint/scope-manager': 7.14.1 '@typescript-eslint/scope-manager': 7.14.1
'@typescript-eslint/types': 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 '@typescript-eslint/visitor-keys': 7.14.1
debug: 4.3.5 debug: 4.3.5
eslint: 8.57.0 eslint: 8.57.0
optionalDependencies: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2685,21 +2691,21 @@ snapshots:
'@typescript-eslint/types': 7.14.1 '@typescript-eslint/types': 7.14.1
'@typescript-eslint/visitor-keys': 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: dependencies:
'@typescript-eslint/typescript-estree': 7.14.1(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.2) '@typescript-eslint/utils': 7.14.1(eslint@8.57.0)(typescript@5.5.3)
debug: 4.3.5 debug: 4.3.5
eslint: 8.57.0 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: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/types@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)':
dependencies: dependencies:
'@typescript-eslint/types': 7.14.1 '@typescript-eslint/types': 7.14.1
'@typescript-eslint/visitor-keys': 7.14.1 '@typescript-eslint/visitor-keys': 7.14.1
@ -2708,18 +2714,18 @@ snapshots:
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
semver: 7.6.2 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: optionalDependencies:
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@typescript-eslint/scope-manager': 7.14.1 '@typescript-eslint/scope-manager': 7.14.1
'@typescript-eslint/types': 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 eslint: 8.57.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3084,11 +3090,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: 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: 8.57.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
transitivePeerDependencies: transitivePeerDependencies:
@ -3101,9 +3107,9 @@ snapshots:
eslint: 8.57.0 eslint: 8.57.0
eslint-compat-utils: 0.5.1(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: 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 debug: 4.3.5
doctrine: 3.0.0 doctrine: 3.0.0
eslint: 8.57.0 eslint: 8.57.0
@ -3704,6 +3710,8 @@ snapshots:
picocolors@1.0.0: {} picocolors@1.0.0: {}
picocolors@1.0.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}
pify@2.3.0: {} pify@2.3.0: {}
@ -3718,45 +3726,45 @@ snapshots:
pluralize@8.0.0: {} pluralize@8.0.0: {}
postcss-import@15.1.0(postcss@8.4.38): postcss-import@15.1.0(postcss@8.4.39):
dependencies: dependencies:
postcss: 8.4.38 postcss: 8.4.39
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
read-cache: 1.0.0 read-cache: 1.0.0
resolve: 1.22.8 resolve: 1.22.8
postcss-js@4.0.1(postcss@8.4.38): postcss-js@4.0.1(postcss@8.4.39):
dependencies: dependencies:
camelcase-css: 2.0.1 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: dependencies:
lilconfig: 3.1.1 lilconfig: 3.1.1
yaml: 2.4.1 yaml: 2.4.1
optionalDependencies: 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: dependencies:
postcss: 8.4.38 postcss: 8.4.39
postcss-selector-parser: 6.0.16 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: dependencies:
postcss: 8.4.38 postcss: 8.4.39
postcss-selector-parser@6.0.16: postcss-selector-parser@6.1.0:
dependencies: dependencies:
cssesc: 3.0.0 cssesc: 3.0.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {} postcss-value-parser@4.2.0: {}
postcss@8.4.38: postcss@8.4.39:
dependencies: dependencies:
nanoid: 3.3.7 nanoid: 3.3.7
picocolors: 1.0.0 picocolors: 1.0.1
source-map-js: 1.2.0 source-map-js: 1.2.0
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
@ -3981,12 +3989,12 @@ snapshots:
normalize-path: 3.0.0 normalize-path: 3.0.0
object-hash: 3.0.0 object-hash: 3.0.0
picocolors: 1.0.0 picocolors: 1.0.0
postcss: 8.4.38 postcss: 8.4.39
postcss-import: 15.1.0(postcss@8.4.38) postcss-import: 15.1.0(postcss@8.4.39)
postcss-js: 4.0.1(postcss@8.4.38) postcss-js: 4.0.1(postcss@8.4.39)
postcss-load-config: 4.0.2(postcss@8.4.38) postcss-load-config: 4.0.2(postcss@8.4.39)
postcss-nested: 6.0.1(postcss@8.4.38) postcss-nested: 6.0.1(postcss@8.4.39)
postcss-selector-parser: 6.0.16 postcss-selector-parser: 6.1.0
resolve: 1.22.8 resolve: 1.22.8
sucrase: 3.35.0 sucrase: 3.35.0
transitivePeerDependencies: transitivePeerDependencies:
@ -4024,9 +4032,9 @@ snapshots:
tree-kill@1.2.2: {} 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: dependencies:
typescript: 5.5.2 typescript: 5.5.3
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
@ -4038,7 +4046,7 @@ snapshots:
tslib@2.6.3: {} 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: dependencies:
bundle-require: 4.0.2(esbuild@0.21.5) bundle-require: 4.0.2(esbuild@0.21.5)
cac: 6.7.14 cac: 6.7.14
@ -4048,15 +4056,15 @@ snapshots:
execa: 5.1.1 execa: 5.1.1
globby: 11.1.0 globby: 11.1.0
joycon: 3.1.1 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 resolve-from: 5.0.0
rollup: 4.13.2 rollup: 4.13.2
source-map: 0.8.0-beta.0 source-map: 0.8.0-beta.0
sucrase: 3.35.0 sucrase: 3.35.0
tree-kill: 1.2.2 tree-kill: 1.2.2
optionalDependencies: optionalDependencies:
postcss: 8.4.38 postcss: 8.4.39
typescript: 5.5.2 typescript: 5.5.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- ts-node - ts-node
@ -4073,9 +4081,9 @@ snapshots:
type-fest@0.8.1: {} 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: {} ufo@1.5.3: {}
@ -4118,7 +4126,7 @@ snapshots:
vite@5.3.2(@types/node@20.14.9): vite@5.3.2(@types/node@20.14.9):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.4.38 postcss: 8.4.39
rollup: 4.13.2 rollup: 4.13.2
optionalDependencies: optionalDependencies:
'@types/node': 20.14.9 '@types/node': 20.14.9

View File

@ -28,6 +28,7 @@ await Promise.all([
build({ build({
...tsupConfig, ...tsupConfig,
entry: ["src/index.ts"], entry: ["src/index.ts"],
external: ["postcss-selector-parser", "postcss"],
}), }),
Bun.write( Bun.write(
"dist/package.json", "dist/package.json",

View 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));
});
});

View 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">',
});
}
});
});

View 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
View 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
View 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),
}),
};
};
}

View File

@ -7,46 +7,8 @@ import { type NodePath, type types as t } from "@babel/core";
import type { SourceLocation, StyleMapEntry } from "./shared"; import type { SourceLocation, StyleMapEntry } from "./shared";
import { type ResolveTailwindOptions, getClassName } from "./index"; 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; export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
type Type = "css" | "js";
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));
export function babelTailwind( export function babelTailwind(
{ {
@ -54,77 +16,37 @@ export function babelTailwind(
clsx, clsx,
getClassName: getClass = getClassName, getClassName: getClass = getClassName,
macroFunction, macroFunction,
macroStyleFunction,
jsxAttributeAction = "delete", jsxAttributeAction = "delete",
jsxAttributeName = "css", jsxAttributeName = "css",
vite, vite,
}: ResolveTailwindOptions, }: ResolveTailwindOptions,
onCollect: ClassNameCollector | undefined onCollect: ClassNameCollector | undefined
) { ) {
function getClsxImport(t: typeof babel.types, cx: t.Identifier) { type BabelPluginState = ReturnType<typeof getState>;
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(paths: NodePath[]) { function getState(
return paths path: NodePath<t.Program>,
.flatMap(path => { state: babel.PluginPass,
const { confident, value } = path.evaluate(); t: typeof babel.types
invariant(confident, `${macroFunction} 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(`${macroFunction} argument has an invalid type`);
})
.join(" ");
}
return definePlugin<BabelPluginState>(({ types: t }) => ({
Program: {
enter(path, state) {
let cx: t.Identifier; let cx: t.Identifier;
const map = (state.tailwindMap = new Map()); let styleImport: t.Identifier;
state.sliceText = node => ({
const cssMap = new Map<string, StyleMapEntry>();
const jsMap = new Map<string, StyleMapEntry>();
function getStyleImport() {
styleImport ??= path.scope.generateUidIdentifier("styles");
return t.cloneNode(styleImport);
}
return {
getClass(type: Type, value: string) {
return type === "css" ? getClass(value) : "tw_" + hash(value);
},
sliceText: (node: t.Node): SourceLocation => ({
filename: state.filename!, filename: state.filename!,
start: node.loc!.start, start: node.loc!.start,
end: node.loc!.end, end: node.loc!.end,
@ -132,90 +54,134 @@ export function babelTailwind(
.split("\n") .split("\n")
.slice(node.loc!.start.line - 1, node.loc!.end.line) .slice(node.loc!.start.line - 1, node.loc!.end.line)
.join("\n"), .join("\n"),
}); }),
state.recordIfAbsent = entry => {
recordIfAbsent(type: Type, entry: StyleMapEntry) {
const map = type === "css" ? cssMap : jsMap;
if (!map.has(entry.key)) { if (!map.has(entry.key)) {
map.set(entry.key, entry); 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);
};
}, },
exit({ node }, { filename, tailwindMap }) { replaceWithImport(type: Type, path: NodePath, className: string) {
if (!tailwindMap.size) return; 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"); invariant(filename, "babel: missing state.filename");
if (cssMap.size) {
const cssName = basename(filename, extname(filename)) + ".css"; const cssName = basename(filename, extname(filename)) + ".css";
const path = join(dirname(filename), cssName); const path = join(dirname(filename), cssName);
const value = Array.from(tailwindMap.values()); const value = Array.from(cssMap.values());
const importee = `tailwind:./${cssName}` + getSuffix(vite, value);
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))); node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
styleMap.set(path, value); styleMap.set(path, value);
onCollect?.(path, value); onCollect?.(path, value);
}
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);
node.body.unshift(
t.importDeclaration(
[t.importNamespaceSpecifier(getStyleImport())],
t.stringLiteral(importee)
)
);
styleMap.set(path, value);
onCollect?.(path, value);
}
},
};
}
const getType = (fnName: string): Type | undefined =>
fnName === macroFunction ? "css" : fnName === macroStyleFunction ? "js" : undefined;
return definePlugin<BabelPluginState>(({ types: t }) => ({
Program: {
enter(path, state) {
Object.assign(state, getState(path, state, t));
},
exit({ node }, _) {
_.finish(node);
}, },
}, },
TaggedTemplateExpression(path, { sliceText, recordIfAbsent }) { TaggedTemplateExpression(path, _) {
if (macroFunction == null) return; if (macroFunction == null && macroStyleFunction == null) return;
const { node } = path; const { node } = path;
const { const {
tag, tag,
quasi: { quasis, expressions }, quasi: { quasis, expressions },
} = node; } = node;
if (!t.isIdentifier(tag, { name: macroFunction })) return; if (!t.isIdentifier(tag)) return;
invariant( invariant(
!expressions.length, !expressions.length,
`${macroFunction}\`\` should not contain expressions` `${macroFunction}\`\` should not contain expressions`
); );
const type = getType(tag.name);
if (!type) return;
const value = quasis[0].value.cooked; const value = quasis[0].value.cooked;
if (value) { if (value) {
const trimmed = trim(value); const trimmed = trim(value);
const className = getClass(trimmed); const className = _.getClass(type, trimmed);
recordIfAbsent({ _.recordIfAbsent(type, {
key: className, key: className,
className: trimmed, 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; if (macroFunction == null) return;
const { node } = path; const { node } = path;
const { callee } = node; 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); const className = getClass(trimmed);
recordIfAbsent({ _.recordIfAbsent(type, {
key: className, key: className,
className: trimmed, 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; const { name } = path.node;
if (name.name !== jsxAttributeName) return; if (name.name !== jsxAttributeName) return;
@ -238,10 +204,10 @@ export function babelTailwind(
const trimmed = trim(value); const trimmed = trim(value);
if (trimmed) { if (trimmed) {
const className = getClass(trimmed); const className = getClass(trimmed);
recordIfAbsent({ _.recordIfAbsent("css", {
key: className, key: className,
className: trimmed, className: trimmed,
location: sliceText(node), location: _.sliceText(node),
}); });
path.replaceWith(t.stringLiteral(className)); path.replaceWith(t.stringLiteral(className));
} }
@ -252,12 +218,12 @@ export function babelTailwind(
} }
}, },
ObjectExpression(path) { ObjectExpression(path) {
const trimmed = evaluateArgs([path]); const trimmed = evaluateArgs(path).join(" ");
const className = getClass(trimmed); const className = getClass(trimmed);
recordIfAbsent({ _.recordIfAbsent("css", {
key: className, key: className,
className: trimmed, className: trimmed,
location: sliceText(path.node), location: _.sliceText(path.node),
}); });
path.replaceWith(t.stringLiteral(className)); path.replaceWith(t.stringLiteral(className));
}, },
@ -292,7 +258,7 @@ export function babelTailwind(
if (classNameAttribute) { if (classNameAttribute) {
const attrValue = classNameAttribute.value!; const attrValue = classNameAttribute.value!;
const wrap = (originalValue: babel.types.Expression) => 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 both are string literals, we can merge them directly here
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) { if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
@ -313,7 +279,7 @@ export function babelTailwind(
parent.attributes.push( parent.attributes.push(
t.jsxAttribute( t.jsxAttribute(
t.jsxIdentifier("className"), 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
View 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;
}

View File

@ -19,7 +19,7 @@ export const babelPlugin = ({
}): esbuild.Plugin => ({ }): esbuild.Plugin => ({
name: "babel-plugin", name: "babel-plugin",
setup(build) { setup(build) {
build.onLoad({ filter }, ({ path }) => { build.onLoad({ filter, namespace: "file" }, ({ path }) => {
const load = once(() => readFileSync(path, "utf-8")); const load = once(() => readFileSync(path, "utf-8"));
const plugins = Array.isArray(getPlugins) const plugins = Array.isArray(getPlugins)
? getPlugins ? getPlugins

View File

@ -1,16 +1,25 @@
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import type * as esbuild from "esbuild"; import type * as esbuild from "esbuild";
import { CssSyntaxError } from "postcss"; 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 PLUGIN_NAME = "tailwind";
const ESBUILD_NAMESPACE = "babel-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, name: PLUGIN_NAME,
setup(build) { setup(build) {
build.onResolve({ filter: /^tailwind:.+\.css$/ }, ({ path, importer }) => { build.onResolve({ filter: /^tailwind:.+\.\w+$/ }, ({ path, importer }) => {
const resolved = join(dirname(importer), path.replace(/^tailwind:/, "")); const resolved = join(dirname(importer), path.replace(/^tailwind:/, ""));
if (styleMap.has(resolved)) { if (styleMap.has(resolved)) {
return { return {
@ -38,11 +47,8 @@ export const esbuildPlugin = (styleMap: StyleMap, compile: Compile): esbuild.Plu
const styles = styleMap.get(path)!; const styles = styleMap.get(path)!;
try { try {
const result = await compile(toCSSText(styles)); const [loader, contents] = await buildStyleFile(path);
return { return { contents, loader };
contents: result,
loader: "css",
};
} catch (e) { } catch (e) {
if (e instanceof CssSyntaxError) { if (e instanceof CssSyntaxError) {
const lines = e.source!.split("\n"); const lines = e.source!.split("\n");

View File

@ -1,28 +1,10 @@
/* eslint-disable unicorn/string-content */ /* eslint-disable unicorn/string-content */
import { promises as fs } from "node:fs"; import { describe, expect, it } from "vitest";
import { resolve } from "node:path"; import { getClassName } from "./index";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { getBuild } from "./__tests__/utils";
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");
describe("babel-tailwind", () => { describe("babel-tailwind", () => {
beforeEach(async () => { const compileESBuild = getBuild("main");
await fs.mkdir(folder, { recursive: true });
});
afterEach(async () => {
await fs.rm(folder, { recursive: true, force: true });
});
it("supports ESBuild", async () => { it("supports ESBuild", async () => {
const { files } = await compileESBuild({ const { files } = await compileESBuild({
@ -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 () => { it("supports custom jsxAttributeName", async () => {
const { files } = await compileESBuild({ const { files } = await compileESBuild({
clsx: "emotion", clsx: "emotion",
@ -159,51 +110,6 @@ describe("babel-tailwind", () => {
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`); 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 () => { it("supports grouped array css attribute", async () => {
const { files } = await compileESBuild({ const { files } = await compileESBuild({
clsx: "emotion", clsx: "emotion",
@ -252,76 +158,4 @@ describe("babel-tailwind", () => {
expect(files.js.text).toContain(`className: isCenter ? "${clsName}" : void 0`); expect(files.js.text).toContain(`className: isCenter ? "${clsName}" : void 0`);
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`); 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),
}),
};
}

View File

@ -7,11 +7,15 @@ import { type ClassNameCollector, babelTailwind } from "./babel-tailwind";
import { esbuildPlugin } from "./esbuild-postcss"; import { esbuildPlugin } from "./esbuild-postcss";
import { vitePlugin } from "./vite-plugin"; import { vitePlugin } from "./vite-plugin";
import { type StyleMap, createPostCSS } from "./shared"; import { type StyleMap, createPostCSS } from "./shared";
import { convertCssToJS } from "./css-to-js";
export { babelPlugin } from "./esbuild-babel"; export { babelPlugin } from "./esbuild-babel";
export { createPostCSS } from "./shared"; export { createPostCSS } from "./shared";
type GetClassName = (className: string) => string; type GetClassName = (className: string) => string;
export type BuildStyleFile = (
path: string
) => Promise<readonly ["css", string] | readonly ["js", string]>;
interface RecursiveStringObject { interface RecursiveStringObject {
[modifier: string]: string | RecursiveStringObject; [modifier: string]: string | RecursiveStringObject;
@ -20,7 +24,7 @@ interface RecursiveStringObject {
export type CSSAttributeValue = string | (string | 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` * @example "tw" => tw`p-2 text-center`
*/ */
export interface TailwindFunction { export interface TailwindFunction {
@ -28,6 +32,15 @@ export interface TailwindFunction {
(...args: (string | RecursiveStringObject)[]): string; (...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 { export interface TailwindPluginOptions {
/** /**
* Tailwind CSS configuration * Tailwind CSS configuration
@ -62,14 +75,21 @@ export interface TailwindPluginOptions {
jsxAttributeAction?: "delete" | "preserve" | ["rename", string]; jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
/** /**
* Template macro function to use for Tailwind classes * Template macro function to combine Tailwind classes
* @default "tw"
* @example * @example
* declare const tw: TailwindFunction; * declare const tw: TailwindFunction;
* "tw" => tw`p-2 text-center` * "tw" => tw`p-2 text-center`
*/ */
macroFunction?: string | undefined; 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. * The prefix to use for the generated class names.
* @default className => `tw-${hash(className)}` * @default className => `tw-${hash(className)}`
@ -108,6 +128,7 @@ export type ResolveTailwindOptions = SetRequired<
| "styleMap" | "styleMap"
| "tailwindConfig" | "tailwindConfig"
| "macroFunction" | "macroFunction"
| "macroStyleFunction"
>; >;
/** /**
@ -140,6 +161,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
postCSSPlugins: [], postCSSPlugins: [],
styleMap: new Map(), styleMap: new Map(),
macroFunction: undefined, macroFunction: undefined,
macroStyleFunction: undefined,
tailwindConfig: {}, tailwindConfig: {},
...options, ...options,
}; };
@ -149,12 +171,27 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
const { styleMap } = resolvedOptions; const { styleMap } = resolvedOptions;
const compile = options.compile ?? memoize(getCompiler()); 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 { return {
compile, compile,
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect), babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
esbuild: () => esbuildPlugin(styleMap, compile), esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
/** Requires `options.vite` to be `true`. */ /** Requires `options.vite` to be `true`. */
vite: () => vitePlugin(styleMap, compile), vite: () => vitePlugin({ styleMap, compile, buildStyleFile }),
styleMap, styleMap,
options, options,
getCompiler, getCompiler,

View File

@ -52,3 +52,9 @@ export function toCSSText(tailwindMap: StyleMapEntry[]) {
.map(({ className, key }) => `.${key} {\n @apply ${className}\n}`) .map(({ className, key }) => `.${key} {\n @apply ${className}\n}`)
.join("\n"); .join("\n");
} }
export function toJSText(tailwindMap: StyleMapEntry[]) {
return tailwindMap
.map(({ className, key }) => `"${key}": "${className}"`)
.join(",\n ");
}

View File

@ -1,10 +1,19 @@
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import type * as vite from "vite"; 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:"; 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", name: "tailwind",
resolveId(id, importer) { resolveId(id, importer) {
if (id === `${pkgName}/base`) { if (id === `${pkgName}/base`) {
@ -35,7 +44,7 @@ export const vitePlugin = (styleMap: StyleMap, compile: Compile): vite.Plugin =>
const name = resolved.split("?")[0]; const name = resolved.split("?")[0];
if (styleMap.has(name)) { if (styleMap.has(name)) {
return await compile(toCSSText(styleMap.get(name)!)); return (await buildStyleFile(name))[1];
} }
} }
}, },