├── .eslintrc.js
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ └── main.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .prettierrc
├── LICENSE
├── README.md
├── buildPack.js
├── commitlint.config.js
├── package.json
├── src
├── EasyType.ts
├── index.spec.ts
├── index.ts
└── transform
│ ├── baseReplacer
│ ├── baseReplacer.spec.ts
│ ├── extractor.ts
│ ├── generateEasyTailwindRegex.spec.ts
│ ├── generateEasyTailwindRegex.ts
│ └── index.ts
│ ├── index.ts
│ ├── react
│ ├── index.ts
│ └── reactReplacer.spec.ts
│ └── transform.spec.ts
├── tsconfig.build.json
├── tsconfig.json
├── tsconfig.nocomments.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: [
18 | '.eslintrc.js', 'node_modules/', 'coverage/', 'dist/', 'performance/'
19 | ],
20 | rules: {
21 | '@typescript-eslint/interface-name-prefix': 'off',
22 | '@typescript-eslint/explicit-function-return-type': 'off',
23 | '@typescript-eslint/explicit-module-boundary-types': 'off',
24 | '@typescript-eslint/no-explicit-any': 'off',
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | # At 00:00 on day-of-month 1.
22 | - cron: '0 0 1 * *'
23 |
24 | jobs:
25 | analyze:
26 | name: Analyze
27 | runs-on: ubuntu-latest
28 | permissions:
29 | actions: read
30 | contents: read
31 | security-events: write
32 |
33 | strategy:
34 | fail-fast: false
35 | matrix:
36 | language: [ 'javascript' ]
37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
38 | # Learn more:
39 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
40 |
41 | steps:
42 | - name: Checkout repository
43 | uses: actions/checkout@v2
44 |
45 | # Initializes the CodeQL tools for scanning.
46 | - name: Initialize CodeQL
47 | uses: github/codeql-action/init@v1
48 | with:
49 | languages: ${{ matrix.language }}
50 | # If you wish to specify custom queries, you can do so here or in a config file.
51 | # By default, queries listed here will override any specified in a config file.
52 | # Prefix the list here with "+" to use these queries and those in the config file.
53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
54 |
55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
56 | # If this step fails, then you should remove it and run the build manually (see below)
57 | - name: Autobuild
58 | uses: github/codeql-action/autobuild@v1
59 |
60 | # ℹ️ Command-line programs to run using the OS shell.
61 | # 📚 https://git.io/JvXDl
62 |
63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
64 | # and modify them (or add more) to build your code if your project
65 | # uses a compiled language
66 |
67 | #- run: |
68 | # make bootstrap
69 | # make release
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v1
73 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | types: [ opened, synchronize ]
6 |
7 | jobs:
8 | build:
9 | name: Build
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Setup
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: 18
18 | cache: 'yarn'
19 |
20 | - name: Install
21 | run: yarn install --prefer-offline
22 | - name: Linter
23 | run: yarn lint
24 | - name: Tests
25 | run: yarn test:ci
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff:
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 | .idea/dictionaries
10 |
11 | # Sensitive or high-churn files:
12 | .idea/**/dataSources/
13 | .idea/**/dataSources.ids
14 | .idea/**/dataSources.xml
15 | .idea/**/dataSources.local.xml
16 | .idea/**/sqlDataSources.xml
17 | .idea/**/dynamic.xml
18 | .idea/**/uiDesigner.xml
19 |
20 | # Gradle:
21 | .idea/**/gradle.xml
22 | .idea/**/libraries
23 |
24 | # CMake
25 | cmake-build-debug/
26 |
27 | # Mongo Explorer plugin:
28 | .idea/**/mongoSettings.xml
29 |
30 | ## File-based project format:
31 | *.iws
32 |
33 | ## Plugin-specific files:
34 |
35 | # IntelliJ
36 | out/
37 |
38 | # mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # Cursive Clojure plugin
45 | .idea/replstate.xml
46 |
47 | # Crashlytics plugin (for Android Studio and IntelliJ)
48 | com_crashlytics_export_strings.xml
49 | crashlytics.properties
50 | crashlytics-build.properties
51 | fabric.properties
52 | ### VisualStudio template
53 | ## Ignore Visual Studio temporary files, build results, and
54 | ## files generated by popular Visual Studio add-ons.
55 | ##
56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
57 |
58 | # User-specific files
59 | *.suo
60 | *.user
61 | *.userosscache
62 | *.sln.docstates
63 |
64 | # User-specific files (MonoDevelop/Xamarin Studio)
65 | *.userprefs
66 |
67 | # Build results
68 | [Dd]ebug/
69 | [Dd]ebugPublic/
70 | [Rr]elease/
71 | [Rr]eleases/
72 | x64/
73 | x86/
74 | bld/
75 | [Bb]in/
76 | [Oo]bj/
77 | [Ll]og/
78 |
79 | # Visual Studio 2015 cache/options directory
80 | .vs/
81 | # Uncomment if you have tasks that create the project's static files in wwwroot
82 | #wwwroot/
83 |
84 | # MSTest test Results
85 | [Tt]est[Rr]esult*/
86 | [Bb]uild[Ll]og.*
87 |
88 | # NUNIT
89 | *.VisualState.xml
90 | TestResult.xml
91 |
92 | # Build Results of an ATL Project
93 | [Dd]ebugPS/
94 | [Rr]eleasePS/
95 | dlldata.c
96 |
97 | # Benchmark Results
98 | BenchmarkDotNet.Artifacts/
99 |
100 | # .NET Core
101 | project.lock.json
102 | project.fragment.lock.json
103 | artifacts/
104 | **/Properties/launchSettings.json
105 |
106 | *_i.c
107 | *_p.c
108 | *_i.h
109 | *.ilk
110 | *.meta
111 | *.obj
112 | *.pch
113 | *.pdb
114 | *.pgc
115 | *.pgd
116 | *.rsp
117 | *.sbr
118 | *.tlb
119 | *.tli
120 | *.tlh
121 | *.tmp
122 | *.tmp_proj
123 | *.log
124 | *.vspscc
125 | *.vssscc
126 | .builds
127 | *.pidb
128 | *.svclog
129 | *.scc
130 |
131 | # Chutzpah Test files
132 | _Chutzpah*
133 |
134 | # Visual C++ cache files
135 | ipch/
136 | *.aps
137 | *.ncb
138 | *.opendb
139 | *.opensdf
140 | *.sdf
141 | *.cachefile
142 | *.VC.db
143 | *.VC.VC.opendb
144 |
145 | # Visual Studio profiler
146 | *.psess
147 | *.vsp
148 | *.vspx
149 | *.sap
150 |
151 | # Visual Studio Trace Files
152 | *.e2e
153 |
154 | # TFS 2012 Local Workspace
155 | $tf/
156 |
157 | # Guidance Automation Toolkit
158 | *.gpState
159 |
160 | # ReSharper is a .NET coding add-in
161 | _ReSharper*/
162 | *.[Rr]e[Ss]harper
163 | *.DotSettings.user
164 |
165 | # JustCode is a .NET coding add-in
166 | .JustCode
167 |
168 | # TeamCity is a build add-in
169 | _TeamCity*
170 |
171 | # DotCover is a Code Coverage Tool
172 | *.dotCover
173 |
174 | # AxoCover is a Code Coverage Tool
175 | .axoCover/*
176 | !.axoCover/settings.json
177 |
178 | # Visual Studio code coverage results
179 | *.coverage
180 | *.coveragexml
181 |
182 | # NCrunch
183 | _NCrunch_*
184 | .*crunch*.local.xml
185 | nCrunchTemp_*
186 |
187 | # MightyMoose
188 | *.mm.*
189 | AutoTest.Net/
190 |
191 | # Web workbench (sass)
192 | .sass-cache/
193 |
194 | # Installshield output folder
195 | [Ee]xpress/
196 |
197 | # DocProject is a documentation generator add-in
198 | DocProject/buildhelp/
199 | DocProject/Help/*.HxT
200 | DocProject/Help/*.HxC
201 | DocProject/Help/*.hhc
202 | DocProject/Help/*.hhk
203 | DocProject/Help/*.hhp
204 | DocProject/Help/Html2
205 | DocProject/Help/html
206 |
207 | # Click-Once directory
208 | publish/
209 |
210 | # Publish Web Output
211 | *.[Pp]ublish.xml
212 | *.azurePubxml
213 | # Note: Comment the next line if you want to checkin your web deploy settings,
214 | # but database connection strings (with potential passwords) will be unencrypted
215 | *.pubxml
216 | *.publishproj
217 |
218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
219 | # checkin your Azure Web App publish settings, but sensitive information contained
220 | # in these scripts will be unencrypted
221 | PublishScripts/
222 |
223 | # NuGet Packages
224 | *.nupkg
225 | # The packages folder can be ignored because of Package Restore
226 | **/[Pp]ackages/*
227 | # except build/, which is used as an MSBuild target.
228 | !**/[Pp]ackages/build/
229 | # Uncomment if necessary however generally it will be regenerated when needed
230 | #!**/[Pp]ackages/repositories.config
231 | # NuGet v3's project.json files produces more ignorable files
232 | *.nuget.props
233 | *.nuget.targets
234 |
235 | # Microsoft Azure Build Output
236 | csx/
237 | *.build.csdef
238 |
239 | # Microsoft Azure Emulator
240 | ecf/
241 | rcf/
242 |
243 | # Windows Store app package directories and files
244 | AppPackages/
245 | BundleArtifacts/
246 | Package.StoreAssociation.xml
247 | _pkginfo.txt
248 | *.appx
249 |
250 | # Visual Studio cache files
251 | # files ending in .cache can be ignored
252 | *.[Cc]ache
253 | # but keep track of directories ending in .cache
254 | !*.[Cc]ache/
255 |
256 | # Others
257 | ClientBin/
258 | ~$*
259 | *~
260 | *.dbmdl
261 | *.dbproj.schemaview
262 | *.jfm
263 | *.pfx
264 | *.publishsettings
265 | orleans.codegen.cs
266 |
267 | # Since there are multiple workflows, uncomment next line to ignore bower_components
268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
269 | #bower_components/
270 |
271 | # RIA/Silverlight projects
272 | Generated_Code/
273 |
274 | # Backup & report files from converting an old project file
275 | # to a newer Visual Studio version. Backup files are not needed,
276 | # because we have git ;-)
277 | _UpgradeReport_Files/
278 | Backup*/
279 | UpgradeLog*.XML
280 | UpgradeLog*.htm
281 |
282 | # SQL Server files
283 | *.mdf
284 | *.ldf
285 | *.ndf
286 |
287 | # Business Intelligence projects
288 | *.rdl.data
289 | *.bim.layout
290 | *.bim_*.settings
291 |
292 | # Microsoft Fakes
293 | FakesAssemblies/
294 |
295 | # GhostDoc plugin setting file
296 | *.GhostDoc.xml
297 |
298 | # Node.js Tools for Visual Studio
299 | .ntvs_analysis.dat
300 | node_modules/
301 |
302 | # Typescript v1 declaration files
303 | typings/
304 |
305 | # Visual Studio 6 build log
306 | *.plg
307 |
308 | # Visual Studio 6 workspace options file
309 | *.opt
310 |
311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
312 | *.vbw
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # JetBrains Rider
330 | .idea/
331 | *.sln.iml
332 |
333 | # IDE - VSCode
334 | .vscode/*
335 | !.vscode/settings.json
336 | !.vscode/tasks.json
337 | !.vscode/launch.json
338 | !.vscode/extensions.json
339 |
340 | # CodeRush
341 | .cr/
342 |
343 | # Python Tools for Visual Studio (PTVS)
344 | __pycache__/
345 | *.pyc
346 |
347 | # Cake - Uncomment if you are using it
348 | # tools/**
349 | # !tools/packages.config
350 |
351 | # Tabs Studio
352 | *.tss
353 |
354 | # Telerik's JustMock configuration file
355 | *.jmconfig
356 |
357 | # BizTalk build output
358 | *.btp.cs
359 | *.btm.cs
360 | *.odx.cs
361 | *.xsd.cs
362 |
363 | # OpenCover UI analysis results
364 | OpenCover/
365 | coverage/
366 |
367 | ### macOS template
368 | # General
369 | .DS_Store
370 | .AppleDouble
371 | .LSOverride
372 |
373 | # Icon must end with two \r
374 | Icon
375 |
376 | # Thumbnails
377 | ._*
378 |
379 | # Files that might appear in the root of a volume
380 | .DocumentRevisions-V100
381 | .fseventsd
382 | .Spotlight-V100
383 | .TemporaryItems
384 | .Trashes
385 | .VolumeIcon.icns
386 | .com.apple.timemachine.donotpresent
387 |
388 | # Directories potentially created on remote AFP share
389 | .AppleDB
390 | .AppleDesktop
391 | Network Trash Folder
392 | Temporary Items
393 | .apdisk
394 |
395 | =======
396 | # Local
397 | .env
398 | dist
399 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Bruno Noriller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Easy Tailwind
2 |
3 | An easier way of writing Tailwind classes.
4 |
5 | [Example to play around](https://stackblitz.com/edit/easy-tailwind?file=src/App.jsx)
6 |
7 | ## Table of Contents
8 |
9 | - [What this is and what this isn't](#what-this-is-and-what-this-isnt)
10 | - [Installation](#installation)
11 | - [Setup](#setup)
12 | - [How to Use](#how-to-use)
13 | - [Break Lines](#break-lines)
14 | - [Use Objects](#use-objects)
15 | - [Conditional Classes](#conditional-classes)
16 | - [Rules for it to Work](#rules-for-it-to-work)
17 | - [Does it Support XYZ?](#does-it-support-xyz)
18 | - [Final Considerations](#final-considerations)
19 | - [Why "Easy" Tailwind?](#why-easy-tailwind)
20 |
21 | ## What this is and what this isn't
22 |
23 | ### What this isn't
24 |
25 | This is not [`WindiCSS`](https://github.com/windicss/windicss), [`UnoCSS`](https://github.com/unocss/unocss) or any other CSS lib or framework. This isn't meant to replace them.
26 |
27 | This is meant to be used with Tailwind. So, if you're not using Tailwind, you don't want this.
28 |
29 | ### What this might be
30 |
31 | If you use [`classnames`](https://github.com/JedWatson/classnames), [`clsx`](https://github.com/lukeed/clsx/) and other utilities to have conditional classes, then this might be a replacement for them.
32 |
33 | This doesn't cover all cases they do, and you could use all of them in conjunction if you want (they would wrap `e`/`etw` functions).
34 |
35 | But if you just use them for class toggling and use Tailwind, then you might want to consider replacing them with this.
36 |
37 | ### What this is
38 |
39 | This is a utility to be used with Tailwind. If you're using Tailwind, you want to consider using this.
40 |
41 | This is a tool to increase Developer Experience. The Tailwind world-class extension still works, even while writing with EasyTailwind. (It doesn't show the whole CSS class generated when using the modifiers, but it shows the important part.)
42 |
43 | This is a tool for cleaner code. You might not agree, but I developed that in mind.
44 |
45 | This is like "table salt", salt is good but you don't want to cover everything in it.
46 |
47 | If you have just a couple of classes then there's no need to call it. Call it when you have multiple classes, especially with modifiers or when you need to toggle classes.
48 |
49 | Go to: [Table of Contents](#table-of-contents)
50 |
51 | ## Installation
52 |
53 | Install with your preferred manager:
54 |
55 | ```bash
56 | npm i easy-tailwind
57 | ```
58 |
59 | ```bash
60 | yarn add easy-tailwind
61 | ```
62 |
63 | ```bash
64 | pnpm add easy-tailwind
65 | ```
66 |
67 | Go to: [Table of Contents](#table-of-contents)
68 |
69 | ## Setup
70 |
71 | The configuration file is usually located in the `root` of the application and looks something like `tailwind.config.cjs`. The basic configuration you need is:
72 |
73 | ```js
74 | // tailwind.config.cjs
75 | const { content } = require('easy-tailwind/transform');
76 | /** @type {import('tailwindcss').Config} */
77 | module.exports = {
78 | content,
79 | // ...
80 | theme: {
81 | // ...
82 | },
83 | plugins: [
84 | // ...
85 | ],
86 | };
87 | ```
88 |
89 | If you need to override something in the `content` config:
90 |
91 | ```js
92 | // tailwind.config.cjs
93 | const { replacer } = require('easy-tailwind/transform');
94 | /** @type {import('tailwindcss').Config} */
95 | module.exports = {
96 | content: {
97 | files: [
98 | '!node_modules',
99 | './**/*.{js,ts,jsx,tsx,html,vue,svelte,astro}' // here you can specify your own files and directories
100 | ],
101 | transform: {
102 | DEFAULT: replacer, // default is applied to all files types
103 | },
104 | }
105 | // ...
106 | theme: {
107 | // ...
108 | },
109 | plugins: [
110 | // ...
111 | ],
112 | };
113 | ```
114 |
115 | ### Specific configuration for frameworks
116 |
117 | Right now, the only specific one is `React`, instead of importing from `easy-tailwind/transform`, import from `easy-tailwind/transform/react`
118 |
119 | In the example for `React`:
120 |
121 | ```js
122 | // tailwind.config.cjs
123 | const { content } = require('easy-tailwind/transform/react');
124 | /** @type {import('tailwindcss').Config} */
125 | module.exports = {
126 | content,
127 | // ...
128 | theme: {
129 | // ...
130 | },
131 | plugins: [
132 | // ...
133 | ],
134 | };
135 | ```
136 |
137 | Where `content` is equivalent to:
138 |
139 | ```js
140 | // tailwind.config.cjs
141 | const { replacer } = require('easy-tailwind/transform');
142 | /** @type {import('tailwindcss').Config} */
143 | module.exports = {
144 | content: {
145 | files: ['!node_modules', './**/*.{js,ts,jsx,tsx,html}'],
146 | transform: {
147 | DEFAULT: replacer,
148 | },
149 | }
150 | // ...
151 | theme: {
152 | // ...
153 | },
154 | plugins: [
155 | // ...
156 | ],
157 | };
158 | ```
159 |
160 | Go to: [Table of Contents](#table-of-contents)
161 |
162 | ### Renaming the exports
163 |
164 | If you rename the exports to something other than `e` or `etw`, or maybe you want to use only one because you're already using another function with the same name, then you need to change the `replacer` with the `customNameReplacer` (exported from `easy-tailwind/transform`).
165 |
166 | Example:
167 |
168 | ```js
169 | // tailwind.config.cjs
170 | const { customNameReplacer } = require('easy-tailwind/transform');
171 | /** @type {import('tailwindcss').Config} */
172 | module.exports = {
173 | content: {
174 | files: [
175 | '!node_modules',
176 | './**/*.{js,ts,jsx,tsx,html,vue,svelte,astro}' // here you can specify your own files and directories
177 | ],
178 | transform: {
179 | DEFAULT: customNameReplacer('newFuncName1', 'newFuncName2'), // default is applied to all files types
180 | 'some-file-extension': customNameReplacer('etw'), // this one you know you're only using `etw`
181 | 'some-other-file-extension': customNameReplacer('newFuncName'), // this one you know you're only using `newFuncName'
182 | },
183 | }
184 | // ...
185 | theme: {
186 | // ...
187 | },
188 | plugins: [
189 | // ...
190 | ],
191 | };
192 | ```
193 |
194 | Go to: [Table of Contents](#table-of-contents)
195 |
196 | ## How to use
197 |
198 | First, import `e` or `etw`:
199 |
200 | ```js
201 | import { e } from 'easy-tailwind';
202 | // e and etw resolve to the same function
203 | ```
204 |
205 | This is a pure function, so you can be sure that it will always return the same values as you pass the same values:
206 |
207 | ```js
208 | e(
209 | 'some base classes here',
210 | 'breaking the line because',
211 | 'it was getting too long',
212 | {
213 | mod1: 'classes with mod1',
214 | mod2: [
215 | 'classes with only mod2',
216 | {
217 | subMod1: 'nested classes with both',
218 | },
219 | ],
220 | },
221 | );
222 | // this will return:
223 | 'some base classes here breaking the line because it was getting too long mod1:classes mod1:with mod1:mod1 mod2:classes mod2:with mod2:only mod2:mod2 mod2:subMod1:nested mod2:subMod1:classes mod2:subMod1:with mod2:subMod1:both';
224 | ```
225 |
226 | Now use it where you would use the Tailwind classes.
227 |
228 | Example below will use the React syntax, but as long as you can call it, it will probably work:
229 |
230 | ```js
231 |
251 | EasyTailwind!!!
252 |
253 | ```
254 |
255 | Which is way faster and easier to understand, maintain and debug than:
256 |
257 | ```js
258 | "text-lg font-medium text-black hover:underline hover:decoration-black sm:text-base sm:text-blue-500 sm:hover:decoration-cyan-500 lg:text-2xl lg:text-green-500 lg:hover:decoration-amber-500"
259 | ```
260 |
261 | > ℹ️ Sense of style not included. 🤣
262 |
263 | Go to: [Table of Contents](#table-of-contents)
264 |
265 | ### Break lines
266 |
267 | One of the uses is to "break lines" of the styles.
268 |
269 | For this, just split the classes into multiple strings and put each one in a single line.
270 |
271 | Example:
272 |
273 | ```js
274 |
283 | Multiple lines!
284 |
285 | ```
286 |
287 | Go to: [Table of Contents](#table-of-contents)
288 |
289 | ### Use Objects
290 |
291 | This is what you were waiting for, objects where the keys are applied to whatever the value is, even nested structures.
292 |
293 | Example:
294 |
295 | ```js
296 |
305 | Objects!
306 |
307 | ```
308 |
309 | This will create exactly what you would expect:
310 |
311 | ```js
312 | "sm:text-sm sm:text-blue-500 md:text-base md:text-green-500 lg:text-lg lg:text-black lg:hover:text-red-500"
313 | ```
314 |
315 | Each key (sm, md, lg, [&_li], group-hover, ...) will apply to everything inside the value.
316 |
317 | Each value can be a string, another object, or an array with strings and/or objects.
318 |
319 | > ⚠️ The ordering is applied in the order of the object.
320 | >
321 | > Some Tailwind classes don't work properly depending on the order,
322 | >
323 | > so always check if what you will be building is valid.
324 |
325 | Go to: [Table of Contents](#table-of-contents)
326 |
327 | ### Conditional classes
328 |
329 | One thing we usually need is conditional classes, we got you covered!
330 |
331 | As long as you follow [the rules](#rules-for-it-to-work):
332 |
333 | - Use boolean values for conditional expressions (ternary, &&, ||, ??, etc...)
334 | - Don't add variables other than the boolean for the conditional expressions
335 |
336 | Example:
337 |
338 | ```js
339 | const boolean = Math.random() > 0.5;
340 | // ...
341 |
348 | Conditional classes!
349 |
350 | ```
351 |
352 | Go to: [Table of Contents](#table-of-contents)
353 |
354 | ## Rules for it to work
355 |
356 | 1. Use boolean values for conditional expressions (ternary, &&, ||, ??, etc...)
357 | 2. Don't add variables other than the boolean for the conditional expressions
358 |
359 | Go to: [Table of Contents](#table-of-contents)
360 |
361 | ### Known limitations
362 |
363 | Those limitations are about the `replacer` in the transform.
364 |
365 | Right now, depending on what you try to use in the `e` function, it will not work and you will be given a warning (check the terminal where you're running your `dev` or `build` script).
366 |
367 | When you're inspecting it in the browser, it will show the classes as normal, but it won't work (the function works as normal, but the transformer won't be able to parse it and the classes won't be added and sent to the browser).
368 |
369 | In those cases, you can append them separately from those you use with `EasyTailwind`.
370 |
371 | Examples:
372 |
373 | ```js
374 | className={`${variable} ${Math.random() > 0.5 ? "more" : "less"} ${e("here goes the safe to parse classes")}`}
375 | ```
376 |
377 | ### Why is this necessary?
378 |
379 | Tailwind works with the JIT compiler that can create new classes on the fly and inject them.
380 |
381 | For it to work, they need to scan all the files looking for the classes, but when you use `EasyTailwind`,
382 | you're basically compressing many of the classes you're trying to use. So we need to add an extra step to Tailwind.
383 |
384 | In the Tailwind config (`tailwind.config.cjs`) you add the files it will scan for tailwind classes and a transform that uses a function that will resolve ahead of time what `EasyTailwind` can produce, so Tailwind can inject ahead of time all possible classes.
385 |
386 | However, the more complicated and inclusive you want it to scan for, the more you lose performance (for running in dev mode and for build).
387 |
388 | The best balance to be able to accept having conditional classes while minimizing the impact on performance is to simplify this, looking for only a boolean variable and not something that can be as simple as a variable or as complex as complex can be.
389 |
390 | See more at [Tailwind "Transforming source files"](https://tailwindcss.com/docs/content-configuration#transforming-source-files).
391 |
392 | Go to: [Table of Contents](#table-of-contents)
393 |
394 | ## Does it Support XYZ?
395 |
396 | Probably.
397 |
398 | If you can use `e('tw classes')` and it generates the classes (even if they don't actually work), then just follow the setup part.
399 |
400 | If you want me to add a custom `content` for the framework you're using, feel free to open a PR. =D
401 |
402 | Go to: [Table of Contents](#table-of-contents)
403 |
404 | ## Final Considerations
405 |
406 | These are mostly 'pure' functions, so we don't need to worry about getting "stale".
407 |
408 | More functionalities are welcome, but ultimately this package can have months between any updates.
409 | This doesn't mean that it's "dead", just that it's doing what it needs.
410 |
411 | Today it works with Tailwind v3, I'm not sure if with lower versions or for higher versions.
412 | As long as the `content` part doesn't change, then you can just import and use it.
413 | If it changes, you have the `replacer` for the transformations (as long as it supports it) but expect updates as soon as possible.
414 |
415 | Go to: [Table of Contents](#table-of-contents)
416 |
417 | ### Why "Easy" Tailwind?
418 |
419 | I'm lazy.
420 |
421 | And while I'm already productive using Tailwind, I don't like to keep repeating the same modifiers over and over again.
422 |
423 | So, it's "easy" to type.
424 |
425 | Another thing is about reading the classes. It's easy to get a long string with all classes jumbled together, even with extensions sorting and linting them it's hard to keep in mind everything that's happening at once.
426 |
427 | So, it's "easy" to read.
428 |
429 | And well, naming is hard and I went with the first thing I thought about. =p
430 |
431 | Go to: [Table of Contents](#table-of-contents)
432 |
433 | ## Work with me
434 |
435 |
436 |
437 | ### Hit me up at Discord
438 |
439 |
440 |
441 | ### Or Donate
442 |
443 | - [$5 Nice job! Keep it up.](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=5)
444 | - [$10 I really liked that, thank you!](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=10)
445 | - [$42 This is exactly what I was looking for.](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=42)
446 | - [$1K WOW. Did not know javascript could do that!](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=1000)
447 | - [$5K I need something done ASAP! Can you do it for yesterday?](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=5000)
448 | - [$10K Please consider this: quit your job and work with me!](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD&amount=10000)
449 | - [$??? Shut up and take my money!](https://www.paypal.com/donate/?business=VWNG7KZD9SS4S&no_recurring=0¤cy_code=USD)
450 |
451 | ## That’s it! 👏
452 |
--------------------------------------------------------------------------------
/buildPack.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const fs = require('fs');
3 | const os = require('os');
4 | const exec = require('child_process').execSync;
5 |
6 | // create temp directory
7 | const tempDirectory = fs.mkdtempSync(`${os.tmpdir()}/your-project-tarball-`);
8 | const packageDirectory = `${tempDirectory}/package`;
9 |
10 | // create subfolder package
11 | fs.mkdirSync(packageDirectory);
12 |
13 | // get current directory
14 | const currentDirectory = exec('pwd')
15 | .toString()
16 | .replace(/(\r\n|\n|\r)/gm, '')
17 | .trim();
18 |
19 | // clean output folder
20 | exec(`npx rimraf out/*`);
21 |
22 | // read existing package.json
23 | const packageJSON = require('./package.json');
24 |
25 | // copy all necessary files
26 | exec(`copyfiles README.md CHANGELOG.md LICENSE ${packageDirectory}`);
27 | exec(`copyfiles -u 1 "dist/**/*" ${packageDirectory}`);
28 |
29 | // modify package.json
30 | Reflect.deleteProperty(packageJSON, 'scripts');
31 | Reflect.deleteProperty(packageJSON, 'devDependencies');
32 | // and save it in temp folder
33 | fs.writeFileSync(
34 | `${packageDirectory}/package.json`,
35 | JSON.stringify(packageJSON, null, 2),
36 | );
37 |
38 | // pack everything and send to out folder
39 | exec(
40 | `cd ${packageDirectory} && yarn pack --out %s-%v.tgz && npx copyfiles *.tgz ${currentDirectory}/out`,
41 | );
42 |
43 | // clean after
44 | exec(`npx rimraf ${tempDirectory}`);
45 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "easy-tailwind",
3 | "private": false,
4 | "version": "1.1.0",
5 | "description": "An easier way of writing Tailwind classes.",
6 | "main": "index.js",
7 | "types": "index.d.ts",
8 | "license": "MIT",
9 | "scripts": {
10 | "lint": "eslint --ignore-path .gitignore --fix .",
11 | "test": "jest --watch --verbose",
12 | "test:ci": "jest --verbose",
13 | "test:cov": "jest --coverage --silent --watchAll",
14 | "prepare": "husky install",
15 | "cleanDist": "npx rimraf dist/*",
16 | "build": "tsc",
17 | "prebuild:prod": "$npm_execpath run cleanDist",
18 | "build:prod": "tsc -p ./tsconfig.build.json && tsc -p ./tsconfig.nocomments.json",
19 | "prepackPublish": "$npm_execpath run build:prod",
20 | "packPublish": "node buildPack.js",
21 | "packPublish:dryRun": "$npm_execpath run packPublish && npm publish ./out/*.tgz --dry-run",
22 | "packPublish:publish": "$npm_execpath run packPublish && $npm_execpath publish ./out/*.tgz"
23 | },
24 | "devDependencies": {
25 | "@commitlint/cli": "^12.1.4",
26 | "@commitlint/config-conventional": "^12.1.4",
27 | "@types/jest": "^26.0.22",
28 | "@types/node": "^16.0.0",
29 | "@typescript-eslint/eslint-plugin": "^4.19.0",
30 | "@typescript-eslint/parser": "^4.19.0",
31 | "copyfiles": "^2.4.1",
32 | "eslint": "^7.22.0",
33 | "eslint-config-prettier": "^8.1.0",
34 | "eslint-plugin-prettier": "^3.3.1",
35 | "husky": "^7.0.0",
36 | "jest": "^27.0.6",
37 | "lint-staged": "^11.0.0",
38 | "prettier": "^2.2.1",
39 | "ts-jest": "^27.0.3",
40 | "ts-loader": "^9.2.3",
41 | "ts-node": "^10.0.0",
42 | "tsconfig-paths": "^3.9.0",
43 | "typescript": "^4.2.3"
44 | },
45 | "author": "Bruno Noriller (https://github.com/Noriller)",
46 | "bugs": {
47 | "url": "https://github.com/Noriller/easy-tailwind/issues"
48 | },
49 | "homepage": "https://github.com/Noriller/easy-tailwind",
50 | "keywords": [
51 | "tailwind",
52 | "chakra",
53 | "class",
54 | "clsx",
55 | "classnames"
56 | ],
57 | "jest": {
58 | "moduleFileExtensions": [
59 | "js",
60 | "json",
61 | "ts"
62 | ],
63 | "rootDir": "src",
64 | "testRegex": ".*\\.spec\\.ts$",
65 | "transform": {
66 | "^.+\\.(t|j)s$": "ts-jest"
67 | },
68 | "collectCoverageFrom": [
69 | "**/*.ts"
70 | ],
71 | "coverageDirectory": "../coverage",
72 | "coverageThreshold": {
73 | "global": {
74 | "statements": 100,
75 | "branches": 100,
76 | "functions": 100,
77 | "lines": 100
78 | }
79 | },
80 | "testEnvironment": "node"
81 | },
82 | "lint-staged": {
83 | "*.{js,ts}": "yarn lint"
84 | },
85 | "repository": {
86 | "type": "git",
87 | "url": "git+https://github.com/Noriller/easy-tailwind.git"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/EasyType.ts:
--------------------------------------------------------------------------------
1 | // I couldn't make this type work yet.
2 | // But if possible, I would like to create it
3 | // then, if it's possible, augment it to check for problems
4 | // that might happen in the transform phase
5 | export type EasyType = unknown;
6 |
--------------------------------------------------------------------------------
/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { e, etw } from './';
2 |
3 | describe('EasyTW', () => {
4 | it('exports both "e" and "etw" as options', () => {
5 | expect(e).toBe(etw);
6 | });
7 |
8 | it.each([
9 | { args: [], expected: '' },
10 | { args: [null], expected: '' },
11 | { args: [undefined], expected: '' },
12 | { args: [null, undefined], expected: '' },
13 | { args: [''], expected: '' },
14 | { args: ['class'], expected: 'class' },
15 | { args: ['multiple classes'], expected: 'multiple classes' },
16 | {
17 | args: [['multiple classes', 'in', 'one array']],
18 | expected: 'multiple classes in one array',
19 | },
20 | {
21 | args: [['multiple classes'], ['in'], ['multiples array']],
22 | expected: 'multiple classes in multiples array',
23 | },
24 | {
25 | args: [
26 | 'multiple classes',
27 | [true && 'in', true && 'array', false && 'never'],
28 | ],
29 | expected: 'multiple classes in array',
30 | },
31 | {
32 | args: ['multiple classes', { mod: 'class' }],
33 | expected: 'multiple classes mod:class',
34 | },
35 | {
36 | args: ['multiple classes', { mod: 'class with mods' }],
37 | expected: 'multiple classes mod:class mod:with mod:mods',
38 | },
39 | {
40 | args: ['multiple classes', { mod: ['base', 'other classes'] }],
41 | expected: 'multiple classes mod:base mod:other mod:classes',
42 | },
43 | {
44 | args: [
45 | 'multiple classes',
46 | {
47 | mod1: ['base', 'other classes'],
48 | mod2: ['base', { 'additional-mod': 'other classes' }],
49 | },
50 | ],
51 | expected:
52 | 'multiple classes mod1:base mod1:other mod1:classes mod2:base mod2:additional-mod:other mod2:additional-mod:classes',
53 | },
54 | {
55 | args: [
56 | 'lorem ipsum dolor',
57 | ['amet', 'consectetur adipiscing elit'],
58 | ['Sed sit', 'amet ligula', ['ex', 'Ut']],
59 | ],
60 | expected:
61 | 'lorem ipsum dolor amet consectetur adipiscing elit Sed sit amet ligula ex Ut',
62 | },
63 | {
64 | args: [
65 | {
66 | mod1: [
67 | 'amet',
68 | 'consectetur adipiscing elit',
69 | ['Sed sit', 'amet ligula', ['ex ut', 'lorem ipsum']],
70 | ],
71 | },
72 | ],
73 | expected:
74 | 'mod1:amet mod1:consectetur mod1:adipiscing mod1:elit mod1:Sed mod1:sit mod1:amet mod1:ligula mod1:ex mod1:ut mod1:lorem mod1:ipsum',
75 | },
76 | ])('return $expected when $args', ({ args, expected }) => {
77 | expect(e(...args)).toBe(expected);
78 | });
79 |
80 | it('handles a really complex object', () => {
81 | expect(
82 | e(
83 | 'Lorem ipsum',
84 | 'dolor sit',
85 | ['amet', 'consectetur adipiscing elit'],
86 | ['Sed sit', 'amet ligula', ['ex', 'Ut']],
87 | {
88 | mod1: 'in suscipit metus',
89 | mod2: [
90 | 'vel accumsan',
91 | 'orci',
92 | ['Vivamus sapien', 'neque', ['dictum vel', 'felis maximus']],
93 | ],
94 | mod3: ['luctus', { mod4: 'lorem' }],
95 | mod5: [
96 | 'Fusce malesuada massa',
97 | ['eu turpis finibus'],
98 | {
99 | mod6: [
100 | 'mollis',
101 | {
102 | mod7: [
103 | 'In augue tortor',
104 | {
105 | mod8: [
106 | 'porta eu erat sit amet',
107 | ['tristique', 'ullamcorper', 'arcu'],
108 | ],
109 | },
110 | ],
111 | },
112 | ],
113 | },
114 | ],
115 | },
116 | ),
117 | ).toBe(
118 | 'Lorem ipsum dolor sit amet consectetur adipiscing elit Sed sit amet ligula ex Ut mod1:in mod1:suscipit mod1:metus mod2:vel mod2:accumsan mod2:orci mod2:Vivamus mod2:sapien mod2:neque mod2:dictum mod2:vel mod2:felis mod2:maximus mod3:luctus mod3:mod4:lorem mod5:Fusce mod5:malesuada mod5:massa mod5:eu mod5:turpis mod5:finibus mod5:mod6:mollis mod5:mod6:mod7:In mod5:mod6:mod7:augue mod5:mod6:mod7:tortor mod5:mod6:mod7:mod8:porta mod5:mod6:mod7:mod8:eu mod5:mod6:mod7:mod8:erat mod5:mod6:mod7:mod8:sit mod5:mod6:mod7:mod8:amet mod5:mod6:mod7:mod8:tristique mod5:mod6:mod7:mod8:ullamcorper mod5:mod6:mod7:mod8:arcu',
119 | );
120 | });
121 |
122 | it.each([false && 'anything', undefined && 'anything', null && 'anything'])(
123 | 'handles falsy values',
124 | (falsy) => {
125 | expect(e(falsy)).toBe('');
126 | },
127 | );
128 |
129 | it('accepts alternatives syntax', () => {
130 | // this syntax don't work with the replacers, so it shouldn't be used directly
131 | // (I've learned about having to transform the files afterwards and had a different idea on how it could be used)
132 | // it's possible I'll refactor it since there's no gain to having it
133 |
134 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
135 | // @ts-ignore
136 | expect(e.mod('classes', 'and more classes')).toEqual([
137 | 'mod:classes',
138 | 'mod:and',
139 | 'mod:more',
140 | 'mod:classes',
141 | ]);
142 | expect(e['special-mod']('classes', 'and more classes')).toEqual([
143 | 'special-mod:classes',
144 | 'special-mod:and',
145 | 'special-mod:more',
146 | 'special-mod:classes',
147 | ]);
148 | });
149 |
150 | it('handles an edge case of empty string', () => {
151 | // this should not happen, but when a "prop" of the
152 | // proxy handle is falsy, it defaults to the unproxied object
153 | expect(e['']('anything')).toEqual('anything');
154 | });
155 |
156 | describe('examples from TW docs', () => {
157 | it.each([
158 | {
159 | args: [
160 | 'bg-slate-100 rounded-xl p-8',
161 | {
162 | md: 'flex p-0',
163 | dark: 'bg-slate-800',
164 | },
165 | ],
166 | expected:
167 | 'bg-slate-100 rounded-xl p-8 md:flex md:p-0 dark:bg-slate-800',
168 | },
169 | {
170 | args: [
171 | 'w-24 h-24 rounded-full mx-auto',
172 | {
173 | md: 'w-48 h-auto rounded-none',
174 | },
175 | ],
176 | expected:
177 | 'w-24 h-24 rounded-full mx-auto md:w-48 md:h-auto md:rounded-none',
178 | },
179 | {
180 | args: [
181 | 'pt-6 text-center space-y-4',
182 | {
183 | md: 'p-8 text-left',
184 | },
185 | ],
186 | expected: 'pt-6 text-center space-y-4 md:p-8 md:text-left',
187 | },
188 | {
189 | args: ['text-sky-500', { dark: 'text-sky-400' }],
190 | expected: 'text-sky-500 dark:text-sky-400',
191 | },
192 | {
193 | args: ['text-slate-700', { dark: 'text-slate-500' }],
194 | expected: 'text-slate-700 dark:text-slate-500',
195 | },
196 | {
197 | args: [
198 | 'flex-none w-48 mb-10 relative z-10',
199 | {
200 | before: 'absolute top-1 left-1 w-full h-full bg-teal-400',
201 | },
202 | ],
203 | expected:
204 | 'flex-none w-48 mb-10 relative z-10 before:absolute before:top-1 before:left-1 before:w-full before:h-full before:bg-teal-400',
205 | },
206 | {
207 | args: [
208 | 'relative flex flex-wrap items-baseline pb-6',
209 | {
210 | before: 'bg-black absolute -top-6 bottom-0 -left-60 -right-6',
211 | },
212 | ],
213 | expected:
214 | 'relative flex flex-wrap items-baseline pb-6 before:bg-black before:absolute before:-top-6 before:bottom-0 before:-left-60 before:-right-6',
215 | },
216 | {
217 | args: [
218 | 'relative w-10 h-10 flex items-center justify-center text-black',
219 | {
220 | before: 'absolute z-[-1] top-0.5 left-0.5 w-full h-full',
221 | 'peer-checked': ['bg-black text-white', { before: 'bg-teal-400' }],
222 | },
223 | ],
224 | expected:
225 | 'relative w-10 h-10 flex items-center justify-center text-black before:absolute before:z-[-1] before:top-0.5 before:left-0.5 before:w-full before:h-full peer-checked:bg-black peer-checked:text-white peer-checked:before:bg-teal-400',
226 | },
227 | {
228 | args: [
229 | 'py-6 px-4',
230 | {
231 | sm: 'p-6',
232 | md: 'py-10 px-8',
233 | },
234 | ],
235 | expected: 'py-6 px-4 sm:p-6 md:py-10 md:px-8',
236 | },
237 | {
238 | args: [
239 | 'max-w-4xl mx-auto grid grid-cols-1',
240 | {
241 | lg: 'max-w-5xl gap-x-20 grid-cols-2',
242 | },
243 | ],
244 | expected:
245 | 'max-w-4xl mx-auto grid grid-cols-1 lg:max-w-5xl lg:gap-x-20 lg:grid-cols-2',
246 | },
247 | {
248 | args: [
249 | 'relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0',
250 | {
251 | sm: 'bg-none row-start-2 p-0',
252 | lg: 'row-start-1',
253 | },
254 | ],
255 | expected:
256 | 'relative p-3 col-start-1 row-start-1 flex flex-col-reverse rounded-lg bg-gradient-to-t from-black/75 via-black/0 sm:bg-none sm:row-start-2 sm:p-0 lg:row-start-1',
257 | },
258 | {
259 | args: [
260 | 'mt-1 text-lg font-semibold text-white',
261 | {
262 | sm: ['text-slate-900', { dark: 'text-white' }],
263 | md: 'text-2xl',
264 | },
265 | ],
266 | expected:
267 | 'mt-1 text-lg font-semibold text-white sm:text-slate-900 sm:dark:text-white md:text-2xl',
268 | },
269 | {
270 | args: [
271 | 'text-sm leading-4 font-medium text-white',
272 | {
273 | sm: ['text-slate-500', { dark: 'text-slate-400' }],
274 | },
275 | ],
276 | expected:
277 | 'text-sm leading-4 font-medium text-white sm:text-slate-500 sm:dark:text-slate-400',
278 | },
279 | {
280 | args: [
281 | 'grid gap-4 col-start-1 col-end-3 row-start-1',
282 | {
283 | sm: 'mb-6 grid-cols-4',
284 | lg: 'gap-6 col-start-2 row-end-6 row-span-6 mb-0',
285 | },
286 | ],
287 | expected:
288 | 'grid gap-4 col-start-1 col-end-3 row-start-1 sm:mb-6 sm:grid-cols-4 lg:gap-6 lg:col-start-2 lg:row-end-6 lg:row-span-6 lg:mb-0',
289 | },
290 | {
291 | args: [
292 | 'w-full h-60 object-cover rounded-lg',
293 | {
294 | sm: 'h-52 col-span-2',
295 | lg: 'col-span-full',
296 | },
297 | ],
298 | expected:
299 | 'w-full h-60 object-cover rounded-lg sm:h-52 sm:col-span-2 lg:col-span-full',
300 | },
301 | {
302 | args: [
303 | 'hidden w-full h-52 object-cover rounded-lg',
304 | {
305 | sm: 'block col-span-2',
306 | md: 'col-span-1',
307 | lg: 'row-start-2 col-span-2 h-32',
308 | },
309 | ],
310 | expected:
311 | 'hidden w-full h-52 object-cover rounded-lg sm:block sm:col-span-2 md:col-span-1 lg:row-start-2 lg:col-span-2 lg:h-32',
312 | },
313 | {
314 | args: [
315 | 'hidden w-full h-52 object-cover rounded-lg',
316 | {
317 | md: 'block',
318 | lg: 'row-start-2 col-span-2 h-32',
319 | },
320 | ],
321 | expected:
322 | 'hidden w-full h-52 object-cover rounded-lg md:block lg:row-start-2 lg:col-span-2 lg:h-32',
323 | },
324 | {
325 | args: [
326 | 'mt-4 text-xs font-medium flex items-center row-start-2',
327 | {
328 | sm: 'mt-1 row-start-3',
329 | md: 'mt-2.5',
330 | lg: 'row-start-2',
331 | },
332 | ],
333 | expected:
334 | 'mt-4 text-xs font-medium flex items-center row-start-2 sm:mt-1 sm:row-start-3 md:mt-2.5 lg:row-start-2',
335 | },
336 | {
337 | args: [
338 | 'text-indigo-600 flex items-center',
339 | { dark: 'text-indigo-400' },
340 | ],
341 | expected: 'text-indigo-600 flex items-center dark:text-indigo-400',
342 | },
343 | {
344 | args: ['mr-1 stroke-current', { dark: 'stroke-indigo-500' }],
345 | expected: 'mr-1 stroke-current dark:stroke-indigo-500',
346 | },
347 | {
348 | args: ['mr-1 text-slate-400', { dark: 'text-slate-500' }],
349 | expected: 'mr-1 text-slate-400 dark:text-slate-500',
350 | },
351 | {
352 | args: [
353 | 'mt-4 col-start-1 row-start-3 self-center',
354 | {
355 | sm: 'mt-0 col-start-2 row-start-2 row-span-2',
356 | lg: 'mt-6 col-start-1 row-start-3 row-end-4',
357 | },
358 | ],
359 | expected:
360 | 'mt-4 col-start-1 row-start-3 self-center sm:mt-0 sm:col-start-2 sm:row-start-2 sm:row-span-2 lg:mt-6 lg:col-start-1 lg:row-start-3 lg:row-end-4',
361 | },
362 | {
363 | args: [
364 | 'mt-4 text-sm leading-6 col-start-1',
365 | {
366 | sm: 'col-span-2',
367 | lg: 'mt-6 row-start-4 col-span-1',
368 | dark: 'text-slate-400',
369 | },
370 | ],
371 | expected:
372 | 'mt-4 text-sm leading-6 col-start-1 sm:col-span-2 lg:mt-6 lg:row-start-4 lg:col-span-1 dark:text-slate-400',
373 | },
374 | {
375 | args: [
376 | 'bg-white space-y-4 p-4',
377 | {
378 | sm: 'px-8 py-6',
379 | lg: 'p-4',
380 | xl: 'px-8 py-6',
381 | },
382 | ],
383 | expected:
384 | 'bg-white space-y-4 p-4 sm:px-8 sm:py-6 lg:p-4 xl:px-8 xl:py-6',
385 | },
386 | {
387 | args: [
388 | {
389 | hover: 'bg-blue-400',
390 | },
391 | 'group flex items-center rounded-md bg-blue-500 text-white text-sm font-medium pl-2 pr-3 py-2 shadow-sm',
392 | ],
393 | expected:
394 | 'hover:bg-blue-400 group flex items-center rounded-md bg-blue-500 text-white text-sm font-medium pl-2 pr-3 py-2 shadow-sm',
395 | },
396 | {
397 | args: [
398 | 'absolute left-3 top-1/2 -mt-2.5 text-slate-400 pointer-events-none',
399 | {
400 | 'group-focus-within': 'text-blue-500',
401 | },
402 | ],
403 | expected:
404 | 'absolute left-3 top-1/2 -mt-2.5 text-slate-400 pointer-events-none group-focus-within:text-blue-500',
405 | },
406 | {
407 | args: [
408 | { focus: 'ring-2 ring-blue-500 outline-none' },
409 | 'appearance-none w-full text-sm leading-6 text-slate-900 placeholder-slate-400 rounded-md py-2 pl-10 ring-1 ring-slate-200 shadow-sm',
410 | ],
411 | expected:
412 | 'focus:ring-2 focus:ring-blue-500 focus:outline-none appearance-none w-full text-sm leading-6 text-slate-900 placeholder-slate-400 rounded-md py-2 pl-10 ring-1 ring-slate-200 shadow-sm',
413 | },
414 | {
415 | args: [
416 | 'bg-slate-50 p-4 grid grid-cols-1 gap-4 text-sm leading-6',
417 | {
418 | sm: 'px-8 pt-6 pb-8 grid-cols-2',
419 | lg: 'p-4 grid-cols-1',
420 | xl: 'px-8 pt-6 pb-8 grid-cols-2',
421 | },
422 | ],
423 | expected:
424 | 'bg-slate-50 p-4 grid grid-cols-1 gap-4 text-sm leading-6 sm:px-8 sm:pt-6 sm:pb-8 sm:grid-cols-2 lg:p-4 lg:grid-cols-1 xl:px-8 xl:pt-6 xl:pb-8 xl:grid-cols-2',
425 | },
426 | {
427 | args: [
428 | { hover: 'bg-blue-500 ring-blue-500 shadow-md' },
429 | 'group rounded-md p-3 bg-white ring-1 ring-slate-200 shadow-sm',
430 | ],
431 | expected:
432 | 'hover:bg-blue-500 hover:ring-blue-500 hover:shadow-md group rounded-md p-3 bg-white ring-1 ring-slate-200 shadow-sm',
433 | },
434 | {
435 | args: [
436 | 'grid grid-cols-2 grid-rows-2 items-center',
437 | {
438 | sm: 'block',
439 | lg: 'grid',
440 | xl: 'block',
441 | },
442 | ],
443 | expected:
444 | 'grid grid-cols-2 grid-rows-2 items-center sm:block lg:grid xl:block',
445 | },
446 | {
447 | args: [{ 'group-hover': 'text-white' }, 'font-semibold text-slate-900'],
448 | expected: 'group-hover:text-white font-semibold text-slate-900',
449 | },
450 | {
451 | args: [
452 | 'col-start-2 row-start-1 row-end-3',
453 | {
454 | sm: 'mt-4',
455 | lg: 'mt-0',
456 | xl: 'mt-4',
457 | },
458 | ],
459 | expected: 'col-start-2 row-start-1 row-end-3 sm:mt-4 lg:mt-0 xl:mt-4',
460 | },
461 | {
462 | args: [
463 | 'flex justify-end -space-x-1.5',
464 | {
465 | sm: 'justify-start',
466 | lg: 'justify-end',
467 | xl: 'justify-start',
468 | },
469 | ],
470 | expected:
471 | 'flex justify-end -space-x-1.5 sm:justify-start lg:justify-end xl:justify-start',
472 | },
473 | {
474 | args: [
475 | { hover: 'border-blue-500 border-solid bg-white text-blue-500' },
476 | 'group w-full flex flex-col items-center justify-center rounded-md border-2 border-dashed border-slate-300 text-sm leading-6 text-slate-900 font-medium py-3',
477 | ],
478 | expected:
479 | 'hover:border-blue-500 hover:border-solid hover:bg-white hover:text-blue-500 group w-full flex flex-col items-center justify-center rounded-md border-2 border-dashed border-slate-300 text-sm leading-6 text-slate-900 font-medium py-3',
480 | },
481 | {
482 | args: [
483 | 'bg-white border-slate-100 border-b rounded-t-xl p-4 pb-6 space-y-6',
484 | {
485 | dark: 'bg-slate-800 border-slate-500',
486 | sm: 'p-10 pb-8 space-y-8',
487 | lg: 'p-6 space-y-6',
488 | xl: 'p-10 pb-8 space-y-8',
489 | },
490 | ],
491 | expected:
492 | 'bg-white border-slate-100 border-b rounded-t-xl p-4 pb-6 space-y-6 dark:bg-slate-800 dark:border-slate-500 sm:p-10 sm:pb-8 sm:space-y-8 lg:p-6 lg:space-y-6 xl:p-10 xl:pb-8 xl:space-y-8',
493 | },
494 | {
495 | args: [
496 | 'hidden',
497 | {
498 | sm: 'block',
499 | lg: 'hidden',
500 | xl: 'block',
501 | },
502 | ],
503 | expected: 'hidden sm:block lg:hidden xl:block',
504 | },
505 | {
506 | args: [
507 | 'rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500',
508 | {
509 | dark: 'text-slate-100 ring-0 bg-slate-500',
510 | },
511 | ],
512 | expected:
513 | 'rounded-lg text-xs leading-6 font-semibold px-2 ring-2 ring-inset ring-slate-500 text-slate-500 dark:text-slate-100 dark:ring-0 dark:bg-slate-500',
514 | },
515 | {
516 | args: [
517 | 'relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4',
518 | 'text-sm text-slate-700 font-medium border border-transparent rounded-bl-lg',
519 | {
520 | hover: 'text-slate-500',
521 | focus: 'outline-none shadow-outline-blue border-blue-300 z-10',
522 | },
523 | 'transition ease-in-out duration-150',
524 | ],
525 | expected:
526 | 'relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-slate-700 font-medium border border-transparent rounded-bl-lg hover:text-slate-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 transition ease-in-out duration-150',
527 | },
528 | ])('return $expected when $args', ({ args, expected }) => {
529 | expect(e(...args)).toBe(expected);
530 | });
531 | });
532 | });
533 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { EasyType } from './EasyType';
2 |
3 | function easyTW(...args: EasyType[]) {
4 | return twReducer(args).join(' ');
5 | }
6 |
7 | /**
8 | * EasyTailwind main function. (to be imported as `e` or `etw`)
9 | *
10 | * Check the docs for the full version.
11 | *
12 | * It accepts any number of args:
13 | * - strings
14 | * - The following also accept nesting:
15 | * - arrays
16 | * - objects
17 | * - The keys used will be applied as a modifier to all nested values
18 | * - All of them also accepts conditionals
19 | *
20 | * You can use it as a way to have more readable TW classes, spliting them into lines:
21 | * ```js
22 | * e(
23 | * "my tw classes line 1",
24 | * "my tw classes line 2",
25 | * "my tw classes line 3",
26 | * )
27 | * ```
28 | *
29 | * You can also use to have conditional classes:
30 | * ```js
31 | * e(
32 | * true && "my tw classes line 1",
33 | * false && "my tw classes line 2",
34 | * "my tw classes line 3",
35 | * )
36 | * ```
37 | *
38 | * Finally, you can use to better organize and reduce repetition:
39 | * ```js
40 | * e(
41 | * "my tw classes line 1",
42 | * {
43 | * mod1: "classes with the mod1",
44 | * mod2: [
45 | * "classes with the mod2",
46 | * {
47 | * subMod: "clases with both mod2 and subMod"
48 | * }
49 | * ]
50 | * }
51 | * )
52 | *
53 | * // this will return:
54 | * "my tw classes line 1 mod1:classes mod1:with mod1:the mod1:mod1 mod2:classes mod2:with mod2:the mod2:mod2 mod2:subMod:clases mod2:subMod:with mod2:subMod:both mod2:subMod:mod2 mod2:subMod:and mod2:subMod:subMod"
55 | * ```
56 | */
57 | export const e: (...args: EasyType[]) => string = new Proxy(easyTW, {
58 | get: function (obj, prop: string) {
59 | return prop
60 | ? (...args: EasyType[]) => twReducer(args).map((c) => `${prop}:${c}`)
61 | : obj;
62 | },
63 | });
64 |
65 | function twReducer(args: EasyType[]) {
66 | return (
67 | args.reduce((acc: string[], cur) => {
68 | if (cur === undefined || cur === null || cur === false) return acc;
69 |
70 | if (Array.isArray(cur)) {
71 | acc.push(...cur.filter(Boolean));
72 | } else if (typeof cur === 'object') {
73 | Object.entries(cur).forEach(([key, value]) => {
74 | if (Array.isArray(value)) {
75 | value.flat(Infinity).forEach((v) => {
76 | acc.push(e[key](v));
77 | });
78 | } else {
79 | acc.push(e[key](value));
80 | }
81 | });
82 | } else {
83 | acc.push(...String(cur).split(' '));
84 | }
85 |
86 | return acc;
87 | }, []) as string[]
88 | ).flat(Infinity);
89 | }
90 |
91 | /**
92 | * @namespace
93 | * @borrows e as etw
94 | */
95 | export { e as etw };
96 |
--------------------------------------------------------------------------------
/src/transform/baseReplacer/baseReplacer.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, jest, it, describe } from '@jest/globals';
2 | import { baseReplacer } from '.';
3 |
4 | describe('.baseReplacer()', () => {
5 | const replacer = baseReplacer(/e\(./gis);
6 |
7 | const consoleErrorSpy = jest.spyOn(
8 | globalThis.console,
9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
10 | // @ts-ignore
11 | globalThis.console.error.name,
12 | );
13 |
14 | beforeEach(() => {
15 | jest.resetAllMocks();
16 | });
17 |
18 | it('returns nothing when nothing is passed', () => {
19 | expect(replacer('')).toBe('');
20 | });
21 |
22 | it('return the just the content when no matches are found', () => {
23 | expect(replacer(`"content"`)).toEqual('"content"');
24 | });
25 |
26 | it.each([
27 | `boolean ? "my" : "classes"`,
28 | `!boolean ? "my" : "classes"`,
29 | `!!boolean ? "my" : "classes"`,
30 | ])('replaces a ternary: %s', (content) => {
31 | expect(replacer(`e(${content})`)).toEqual(`e('my classes')`);
32 | });
33 |
34 | it.each([
35 | `boolean && "my classes"`,
36 | `boolean || "my classes"`,
37 | `boolean ?? "my classes"`,
38 | `!boolean && "my classes"`,
39 | `!boolean || "my classes"`,
40 | `!boolean ?? "my classes"`,
41 | `!!boolean && "my classes"`,
42 | `!!boolean || "my classes"`,
43 | `!!boolean ?? "my classes"`,
44 | ])('replaces conditionals: %s', (content) => {
45 | expect(replacer(`e(${content})`)).toEqual(`e('my classes')`);
46 | });
47 |
48 | it.each([
49 | { content: `boolean && ["my", "classes"]`, result: 'my classes' },
50 | {
51 | content: `boolean && {
52 | mod:"my classes"
53 | }`,
54 | result: 'mod:my mod:classes',
55 | },
56 | {
57 | content: `{ my: boolean && [ { mod:"classes" } ] }`,
58 | result: 'my:mod:classes',
59 | },
60 | ])(
61 | 'replaces conditionals with objects: $content | return $result',
62 | ({ content, result }) => {
63 | expect(replacer(`e(${content})`)).toEqual(`e('${result}')`);
64 | },
65 | );
66 |
67 | it.each([
68 | `(my > boolean) ? "something" : "else"`,
69 | `(my > boolean) && "something else"`,
70 | `(my > boolean) || "something else"`,
71 | `(my > boolean) ?? "something else"`,
72 | ])('throws when content dont follow etw rules: %s', (content) => {
73 | expect(replacer(`e(${content})`)).toEqual(`e(${content})`);
74 | expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
75 | expect(consoleErrorSpy).toHaveBeenCalledWith(
76 | `\nAre you following EasyTailwind rules?\n\nmy is not defined in\n${content}\n\nTrying to be transformed into:\n${content}\n`,
77 | );
78 | });
79 |
80 | it('returns the content without throwing when no matchs are found', () => {
81 | const content = 'content';
82 | expect(baseReplacer(/no match/g)(content)).toEqual(content);
83 | expect(consoleErrorSpy).toHaveBeenCalledTimes(0);
84 | });
85 |
86 | it('throws when using regex without global flag', () => {
87 | const content = 'content';
88 | expect(baseReplacer(/no match/)(content)).toEqual(content);
89 | expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
90 | expect(consoleErrorSpy).toHaveBeenCalledWith(
91 | `\nAre you following EasyTailwind rules?\n\nString.prototype.matchAll called with a non-global RegExp argument in file\ncontent\n`,
92 | );
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/transform/baseReplacer/extractor.ts:
--------------------------------------------------------------------------------
1 | type Quote = '"' | "'" | '`';
2 |
3 | /**
4 | * Takes arguments from a RegExp match that finds the use of EasyTailwind.
5 | * From there it iterates the string until finding the closing bracket and
6 | * returns the string.
7 | *
8 | * In other words, this returns whatever was passed as arguments to `e`
9 | */
10 | export function extractArgumentsIndex(
11 | matchIndex: number,
12 | matchLen: number,
13 | wholeText: string,
14 | ) {
15 | const initialIndex = matchIndex + matchLen - 1;
16 | const getQuote = (index): Quote | false =>
17 | ['"', "'", '`'].includes(wholeText[index])
18 | ? (wholeText[index] as Quote)
19 | : false;
20 |
21 | // check if inside a string
22 | let isInString: Quote | false = false;
23 |
24 | // balance round brackets to know when to stop consuming
25 | // 1 => initial bracket
26 | let balanceBracketCheck = 1;
27 |
28 | // check for escapes
29 | const lastChar: string = null;
30 |
31 | // to be found
32 | let finalIndex: number = null;
33 |
34 | for (let i = initialIndex; i < wholeText.length; i++) {
35 | const isQuote = getQuote(i);
36 | if (isQuote) {
37 | if (isInString === isQuote && lastChar !== '\\') {
38 | isInString = false;
39 | } else {
40 | isInString = isQuote;
41 | }
42 | continue;
43 | }
44 |
45 | if (wholeText[i] === '(') {
46 | balanceBracketCheck++;
47 | continue;
48 | }
49 |
50 | if (wholeText[i] === ')') {
51 | if (--balanceBracketCheck === 0) {
52 | finalIndex = i;
53 | break;
54 | }
55 | }
56 | }
57 |
58 | return wholeText.slice(initialIndex, finalIndex);
59 | }
60 |
--------------------------------------------------------------------------------
/src/transform/baseReplacer/generateEasyTailwindRegex.spec.ts:
--------------------------------------------------------------------------------
1 | import { baseReplacer } from '.';
2 | import { generateEasyTailwindRegex } from './generateEasyTailwindRegex';
3 |
4 | describe('.generateEasyTailwindRegex()', () => {
5 | describe('using a custom name', () => {
6 | const replacer = baseReplacer(generateEasyTailwindRegex('myFuncName'));
7 | it('matches "myFuncName"', () => {
8 | const fixture = `
9 | export function MyComponent() {
10 | return anything
;
11 | }
12 | `;
13 |
14 | expect(replacer(fixture)).toMatchInlineSnapshot(`
15 | "
16 | export function MyComponent() {
17 | return anything
;
18 | }
19 | "
20 | `);
21 | });
22 |
23 | it("doesn't match 'e'", async () => {
24 | const fixture = `
25 | export function MyComponent() {
26 | return anything
;
27 | }
28 | `;
29 |
30 | expect(replacer(fixture)).toMatchInlineSnapshot(`
31 | "
32 | export function MyComponent() {
33 | return anything
;
34 | }
35 | "
36 | `);
37 | });
38 | });
39 |
40 | describe('when not passing a function name', () => {
41 | it('throws', () => {
42 | expect(() => generateEasyTailwindRegex()).toThrowError(
43 | 'Please provide at least one function name when using this.',
44 | );
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/transform/baseReplacer/generateEasyTailwindRegex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This generates a RegExp based on a custom name of the export you use.
3 | */
4 | export const generateEasyTailwindRegex = (...functionName: string[]) => {
5 | if (functionName.length === 0) {
6 | throw new Error(
7 | 'Please provide at least one function name when using this.',
8 | );
9 | }
10 |
11 | return new RegExp(
12 | `\\W(?:${functionName
13 | .map((s) => s.trim())
14 | .filter(Boolean)
15 | .join('|')})\\(.`,
16 | 'gis',
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/transform/baseReplacer/index.ts:
--------------------------------------------------------------------------------
1 | import { e } from '../..';
2 | import { extractArgumentsIndex } from './extractor';
3 |
4 | /**
5 | * This matches the start of any use of EasyTailwind
6 | */
7 | const genericRegex = /\W(?:e|etw)\(./gis;
8 |
9 | /**
10 | * This matches the &&, ||, ?? operators that are used for conditional classes.
11 | * The first group is the whole match.
12 | */
13 | const replaceAndOr = /((!*?\w+)\s*?(&&|\|\||\?\?))/gis;
14 |
15 | /**
16 | * This matches the ternary style operator and returns as the 2 groups the true and false matches.
17 | */
18 | const replaceTernary =
19 | /(?:!*?\w+)\s*\?\s*(?['"`])(?.*?)\k\s*:\s*(?['"`])(?.*?)\k/gis;
20 |
21 | /**
22 | * If any of the available transforms aren't suited for your needs,
23 | * then you can use this baseReplacer function to create a replacer
24 | * that will work for you.
25 | *
26 | * This accepts a RegExp where the first group is the args to be passed to `e`/`etw` function (`EasyTailwind` main function).
27 | *
28 | * Then in `tailwind.config.cjs`
29 | *
30 | *```js
31 | module.exports = {
32 | content: {
33 | // ...
34 | transform: {
35 | // ...,
36 | 'my-file-type': baseReplacer(/my regex/),
37 | DEFAULT: baseReplacer(/my regex/), // default is a catch all
38 | }
39 | },
40 | // ...
41 | }
42 | * ```
43 | */
44 | export const baseReplacer = (easyTailwindRegex: RegExp = genericRegex) => {
45 | return (content: string) => {
46 | try {
47 | return [...content.matchAll(easyTailwindRegex)].reduce(
48 | (acc: string, matchArr: RegExpMatchArray) => {
49 | const extractedArgs = extractArgumentsIndex(
50 | matchArr.index,
51 | matchArr[0].length,
52 | content,
53 | );
54 |
55 | const currentTransform = extractedArgs
56 | .replace(replaceAndOr, '')
57 | .replace(replaceTernary, '"$ $"')
58 | .trim();
59 |
60 | try {
61 | const parsedArgs = e(
62 | ...new Function(`return [${currentTransform}]`)(),
63 | );
64 |
65 | return acc.replace(extractedArgs, `'${parsedArgs}'`);
66 | } catch (errorParsing) {
67 | console.error(
68 | `\nAre you following EasyTailwind rules?\n\n${errorParsing.message} in\n${extractedArgs}\n\nTrying to be transformed into:\n${currentTransform}\n`,
69 | );
70 | return acc;
71 | }
72 | },
73 | content,
74 | );
75 | } catch (errorOnFile) {
76 | console.error(
77 | `\nAre you following EasyTailwind rules?\n\n${errorOnFile.message} in file\n${content}\n`,
78 | );
79 | return content;
80 | }
81 | };
82 | };
83 |
--------------------------------------------------------------------------------
/src/transform/index.ts:
--------------------------------------------------------------------------------
1 | import { baseReplacer } from './baseReplacer';
2 | import { generateEasyTailwindRegex } from './baseReplacer/generateEasyTailwindRegex';
3 |
4 | /**
5 | * If you need to change something in the `content`, add or remove file extensions or
6 | * use other transformers, then you can use this.
7 | *
8 | * Then in `tailwind.config.cjs`
9 | *
10 | *```js
11 | module.exports = {
12 | content: {
13 | // ...
14 | transform: {
15 | // ...,
16 | 'my-file-type': replacer,
17 | DEFAULT: replacer, // default is a catch all
18 | }
19 | },
20 | // ...
21 | }
22 | * ```
23 | */
24 | const replacer = baseReplacer();
25 |
26 | /**
27 | * If you're renaming or have collisions with other functions named `e` or `etw`
28 | *
29 | * You can add here all the names you're using. By default it's `e` and `etw`
30 | *
31 | * Example:
32 | *
33 | * ```js
34 | * const replacer = customNameReplacer('e', 'etw');
35 | * ```
36 | */
37 | const customNameReplacer = (...modifiedNames: string[]) => {
38 | // removes empty values
39 | const trimmedNames = modifiedNames.filter(Boolean);
40 |
41 | // returns a string array
42 | const names =
43 | trimmedNames.length === 0 ? ['e', 'etw'] : modifiedNames.map(String);
44 |
45 | return baseReplacer(generateEasyTailwindRegex(...names));
46 | };
47 |
48 | /**
49 | * Catch all `content` key with the most common file extensions.
50 | *
51 | * Import in `tailwind.config.cjs`:
52 | *
53 | * `const { content } = require('easy-tailwind/transform');`
54 | *
55 | * And then:
56 | * ```js
57 | module.exports = {
58 | content,
59 | // ...
60 | }
61 | * ```
62 | *
63 | * It simply returns:
64 | * ```js
65 | {
66 | files: ['!node_modules', '.\/**\/*.{js,ts,jsx,tsx,html,vue,svelte,astro}'],
67 | transform: {
68 | DEFAULT: replacer,
69 | }
70 | }
71 | * ```
72 | *
73 | * Where the `replacer` is also exported from this folder and returns whats needed
74 | * for it to be able to work properly.
75 | *
76 | * If you're renaming the default names, then use `customNameReplacer`.
77 | *
78 | * "DEFAULT" is used in all file types, but you can override each file type by extension name
79 | */
80 | const content = {
81 | files: ['!node_modules', './**/*.{js,ts,jsx,tsx,html,vue,svelte,astro}'],
82 | transform: {
83 | DEFAULT: replacer,
84 | },
85 | };
86 |
87 | export { customNameReplacer, replacer, content };
88 |
--------------------------------------------------------------------------------
/src/transform/react/index.ts:
--------------------------------------------------------------------------------
1 | // istanbul ignore file
2 | import { replacer } from '..';
3 |
4 | /**
5 | * `content` key for use in React apps with Tailwind.
6 | *
7 | * Import in `tailwind.config.cjs`:
8 | *
9 | * `const { content } = require('easy-tailwind/transform/react');`
10 | *
11 | * And then:
12 | * ```js
13 | module.exports = {
14 | content,
15 | // ...
16 | }
17 | * ```
18 | *
19 | * It simply returns:
20 | * ```js
21 | {
22 | files: ['!node_modules', '.\/**\/*.{js,ts,jsx,tsx,html}'],
23 | transform: {
24 | DEFAULT: replacer,
25 | }
26 | }
27 | * ```
28 | *
29 | * Where the `replacer` is exported from the index of the `/transform` folder.
30 | *
31 | * "DEFAULT" is used in all file types, but you can override each file type by extension name
32 | */
33 | export const content = {
34 | files: ['!node_modules', './**/*.{js,ts,jsx,tsx,html}'],
35 | transform: {
36 | DEFAULT: replacer,
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/transform/react/reactReplacer.spec.ts:
--------------------------------------------------------------------------------
1 | import { replacer } from '..';
2 |
3 | describe('react| .replacer()', () => {
4 | const consoleErrorSpy = jest.spyOn(
5 | globalThis.console,
6 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7 | // @ts-ignore
8 | globalThis.console.error.name,
9 | );
10 |
11 | beforeEach(() => {
12 | jest.resetAllMocks();
13 | });
14 |
15 | it('handles a file without matches', () => {
16 | const fixtureWithoutMatch = `
17 | export function MyComponent() {
18 | return anything
;
19 | }
20 | `;
21 |
22 | expect(replacer(fixtureWithoutMatch)).toBe(fixtureWithoutMatch);
23 | });
24 |
25 | it('handles a file with simple matches', async () => {
26 | const fixtureWithSimpleMatches = `
27 | export function MyComponentWithMatches() {
28 | return (
29 |
30 |
anything1
31 |
anything2
32 |
33 | );
34 | }
35 | `;
36 |
37 | expect(replacer(fixtureWithSimpleMatches)).toMatchInlineSnapshot(`
38 | "
39 | export function MyComponentWithMatches() {
40 | return (
41 |
42 |
anything1
43 |
anything2
44 |
45 | );
46 | }
47 | "
48 | `);
49 | });
50 |
51 | it('handles string with linebreaks', async () => {
52 | const fixtureWithSimpleMatches = `
53 | export function MyComponentWithMatches() {
54 | return (
55 |
64 | );
65 | }
66 | `;
67 |
68 | expect(replacer(fixtureWithSimpleMatches)).toMatchInlineSnapshot(`
69 | "
70 | export function MyComponentWithMatches() {
71 | return (
72 |
78 | );
79 | }
80 | "
81 | `);
82 | });
83 |
84 | it('handles a file with a complex usage', () => {
85 | const fixtureWithComplexUsage = `
86 | export function MyComponentWithMatches() {
87 | const random = Math.random() > 0.5;
88 | return (
89 |
107 | )
108 | }
109 | `;
110 | expect(replacer(fixtureWithComplexUsage)).toMatchInlineSnapshot(`
111 | "
112 | export function MyComponentWithMatches() {
113 | const random = Math.random() > 0.5;
114 | return (
115 |
120 | )
121 | }
122 | "
123 | `);
124 | });
125 |
126 | it('handles a file that dont follow etw rules', () => {
127 | const fixtureWithError = `
128 | export function MyComponentWithError() {
129 | return (
130 |
131 |
0.5 ? 'using with e' : 'but with error'
134 | )
135 | }>anything1
136 |
137 | )
138 | }
139 | `;
140 | expect(replacer(fixtureWithError)).toEqual(fixtureWithError);
141 | expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
142 | expect(consoleErrorSpy).toHaveBeenCalledWith(`
143 | Are you following EasyTailwind rules?
144 |
145 | Unexpected string in
146 |
147 | Math.random() > 0.5 ? \'using with e\' : \'but with error\'
148 |
149 |
150 | Trying to be transformed into:
151 | Math.random() > 0."using with e but with error"
152 | `);
153 | });
154 |
155 | it('handles example from playground', () => {
156 | const fixtureWithMatches = `
157 | export function MyComponentWithWeirdThings() {
158 | return (
159 |
166 | )
167 | }
168 | `;
169 | expect(replacer(fixtureWithMatches)).toMatchInlineSnapshot(`
170 | "
171 | export function MyComponentWithWeirdThings() {
172 | return (
173 |
176 | )
177 | }
178 | "
179 | `);
180 | });
181 |
182 | it('handles unbalanced brackets', () => {
183 | const fixtureWithImbalance = `
184 | export function MyComponentWithWeirdThings() {
185 | return (
186 |
197 | )
198 | }
199 | `;
200 | expect(replacer(fixtureWithImbalance)).toMatchInlineSnapshot(`
201 | "
202 | export function MyComponentWithWeirdThings() {
203 | return (
204 |
215 | )
216 | }
217 | "
218 | `);
219 | });
220 |
221 | it('handles etw inside a string template', () => {
222 | const fixtureWithTemplate = `
223 | export function MyComponentWithWeirdThings() {
224 | return (
225 |
233 | )
234 | }
235 | `;
236 | expect(replacer(fixtureWithTemplate)).toMatchInlineSnapshot(`
237 | "
238 | export function MyComponentWithWeirdThings() {
239 | return (
240 |
245 | )
246 | }
247 | "
248 | `);
249 | });
250 | });
251 |
--------------------------------------------------------------------------------
/src/transform/transform.spec.ts:
--------------------------------------------------------------------------------
1 | import { customNameReplacer } from '.';
2 |
3 | describe('.customNameReplacer()', () => {
4 | describe('using a custom name', () => {
5 | const replacer = customNameReplacer('myFuncName');
6 |
7 | it('it generates a replacer with the myFuncName', () => {
8 | const fixture = `
9 | export function MyComponent() {
10 | return (
11 |
12 |
anything
13 |
anything
14 |
anything
15 |
16 | );
17 | }
18 | `;
19 |
20 | expect(replacer(fixture)).toMatchInlineSnapshot(`
21 | "
22 | export function MyComponent() {
23 | return (
24 |
25 |
anything
26 |
anything
27 |
anything
28 |
29 | );
30 | }
31 | "
32 | `);
33 | });
34 |
35 | it("doesn't match 'e' or 'etw'", async () => {
36 | const fixture = `
37 | export function MyComponent() {
38 | return (
39 |
40 |
anything
41 |
anything
42 |
anything
43 |
44 | );
45 | }
46 | `;
47 |
48 | expect(replacer(fixture)).toMatchInlineSnapshot(`
49 | "
50 | export function MyComponent() {
51 | return (
52 |
53 |
anything
54 |
anything
55 |
anything
56 |
57 | );
58 | }
59 | "
60 | `);
61 | });
62 | });
63 |
64 | describe('without passing a custom name', () => {
65 | const replacer = customNameReplacer();
66 |
67 | it('it generates a replacer for the default names', () => {
68 | const fixture = `
69 | export function MyComponent() {
70 | return (
71 |
72 |
anything
73 |
anything
74 |
anything
75 |
76 | );
77 | }
78 | `;
79 |
80 | expect(replacer(fixture)).toMatchInlineSnapshot(`
81 | "
82 | export function MyComponent() {
83 | return (
84 |
85 |
anything
86 |
anything
87 |
anything
88 |
89 | );
90 | }
91 | "
92 | `);
93 | });
94 | });
95 |
96 | describe('passing an empty custom name', () => {
97 | const replacer = customNameReplacer('');
98 |
99 | it('it generates a replacer for the default names', () => {
100 | const fixture = `
101 | export function MyComponent() {
102 | return (
103 |
104 |
anything
105 |
anything
106 |
anything
107 |
108 | );
109 | }
110 | `;
111 |
112 | expect(replacer(fixture)).toMatchInlineSnapshot(`
113 | "
114 | export function MyComponent() {
115 | return (
116 |
117 |
anything
118 |
anything
119 |
anything
120 |
121 | );
122 | }
123 | "
124 | `);
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "performance","test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": false,
6 | "emitDecoratorMetadata": false,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "lib": [
11 | "ES2019"
12 | ],
13 | "sourceMap": true,
14 | "outDir": "./dist",
15 | "baseUrl": "./",
16 | "incremental": false,
17 | "noEmit": false,
18 | "noEmitHelpers": false,
19 | },
20 | "exclude": [
21 | "node_modules",
22 | "performance",
23 | "dist",
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.nocomments.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "removeComments": true,
6 | "sourceMap": false,
7 | }
8 | }
--------------------------------------------------------------------------------