ns-tailwind-vite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/nativescript.vite.mjs +14 -0
- package/package.json +60 -0
- package/src/css-transformer.js +479 -0
- package/src/index.js +8 -0
- package/src/vite-plugin.js +115 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @nativescript/tailwind
|
|
2
|
+
|
|
3
|
+
Makes using [Tailwind CSS v4](https://tailwindcss.com/) in NativeScript a whole lot easier!
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<label
|
|
7
|
+
text="Tailwind CSS is awesome!"
|
|
8
|
+
class="px-2 py-1 text-center text-blue-600 bg-blue-200 rounded-full"
|
|
9
|
+
/>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ✅ **TailwindCSS v4** - Full support for the latest Tailwind
|
|
17
|
+
- ✅ **HMR Support** - Hot Module Replacement works out of the box
|
|
18
|
+
- ✅ **Vite Native** - Uses `@tailwindcss/vite` directly, no PostCSS required
|
|
19
|
+
- ✅ **Auto Transform** - Automatically converts CSS for NativeScript compatibility
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @nativescript/tailwind tailwindcss @tailwindcss/vite
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
### Automatic Setup (Recommended)
|
|
30
|
+
|
|
31
|
+
The plugin auto-configures via `nativescript.vite.mjs`. Just install and import TailwindCSS in your project.
|
|
32
|
+
|
|
33
|
+
Add to your `app.css`:
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
@import "tailwindcss";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's it! Start using Tailwind classes in your app.
|
|
40
|
+
|
|
41
|
+
### Manual Setup
|
|
42
|
+
|
|
43
|
+
If you need more control, configure in `vite.config.ts`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { defineConfig } from "vite";
|
|
47
|
+
import nativescriptTailwind from "@nativescript/tailwind/vite";
|
|
48
|
+
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
plugins: [
|
|
51
|
+
nativescriptTailwind({
|
|
52
|
+
debug: false, // Enable debug logging
|
|
53
|
+
includeTailwind: true, // Include @tailwindcss/vite (set false if adding separately)
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Without TailwindCSS Vite Plugin
|
|
60
|
+
|
|
61
|
+
If you're adding `@tailwindcss/vite` separately:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { defineConfig } from "vite";
|
|
65
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
66
|
+
import nativescriptTailwind from "@nativescript/tailwind/vite";
|
|
67
|
+
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
plugins: [tailwindcss(), nativescriptTailwind({ includeTailwind: false })],
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CSS Transformations
|
|
74
|
+
|
|
75
|
+
The plugin automatically handles:
|
|
76
|
+
|
|
77
|
+
| Transformation | Description |
|
|
78
|
+
| ------------------------ | ----------------------------------------- |
|
|
79
|
+
| `rem/em` → `px` | Converts to pixels (16px base) |
|
|
80
|
+
| `@media` rules | Removed (unsupported in NativeScript) |
|
|
81
|
+
| `@supports` rules | Removed (unsupported in NativeScript) |
|
|
82
|
+
| `@property` rules | Removed (unsupported in NativeScript) |
|
|
83
|
+
| `@layer` blocks | Flattened (lifted to parent) |
|
|
84
|
+
| `:root/:host` | Converted to `.ns-root, .ns-modal` |
|
|
85
|
+
| `::placeholder` | Converted to `placeholder-color` property |
|
|
86
|
+
| `animation` shorthand | Expanded to individual properties |
|
|
87
|
+
| `visibility: hidden` | Converted to `visibility: collapse` |
|
|
88
|
+
| `vertical-align: middle` | Converted to `vertical-align: center` |
|
|
89
|
+
|
|
90
|
+
## NativeScript Root Class
|
|
91
|
+
|
|
92
|
+
Add the `ns-root` class to your root layout for CSS variables to work:
|
|
93
|
+
|
|
94
|
+
```vue
|
|
95
|
+
<template>
|
|
96
|
+
<Frame class="ns-root">
|
|
97
|
+
<Page>
|
|
98
|
+
<!-- Your content -->
|
|
99
|
+
</Page>
|
|
100
|
+
</Frame>
|
|
101
|
+
</template>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Platform Variants
|
|
105
|
+
|
|
106
|
+
Use Tailwind v4's variant syntax with NativeScript platform classes:
|
|
107
|
+
|
|
108
|
+
```css
|
|
109
|
+
/* In your app.css */
|
|
110
|
+
@import "tailwindcss";
|
|
111
|
+
|
|
112
|
+
@custom-variant android (&:where(.ns-android, .ns-android *));
|
|
113
|
+
@custom-variant ios (&:where(.ns-ios, .ns-ios *));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then use in your templates:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<label
|
|
120
|
+
class="android:text-red-500 ios:text-blue-500"
|
|
121
|
+
text="Platform specific!"
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Dark Mode
|
|
126
|
+
|
|
127
|
+
NativeScript applies `.ns-dark` class automatically. Configure in your CSS:
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
@import "tailwindcss";
|
|
131
|
+
|
|
132
|
+
@custom-variant dark (&:where(.ns-dark, .ns-dark *));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Debug Mode
|
|
136
|
+
|
|
137
|
+
Enable debug logging:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
VITE_DEBUG_LOGS=1 ns run ios
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or in plugin options:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
nativescriptTailwind({ debug: true });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Compatibility
|
|
150
|
+
|
|
151
|
+
- NativeScript 8.x+
|
|
152
|
+
- TailwindCSS 4.x
|
|
153
|
+
- Vite 5.x or 6.x
|
|
154
|
+
- @nativescript/vite (latest)
|
|
155
|
+
|
|
156
|
+
## Upgrading from v4 (Tailwind CSS v3)
|
|
157
|
+
|
|
158
|
+
1. Update dependencies:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm install @nativescript/tailwind@latest tailwindcss@latest @tailwindcss/vite
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
2. Replace your `app.css` imports:
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
/* Old v3 way */
|
|
168
|
+
@tailwind base;
|
|
169
|
+
@tailwind components;
|
|
170
|
+
@tailwind utilities;
|
|
171
|
+
|
|
172
|
+
/* New v4 way */
|
|
173
|
+
@import "tailwindcss";
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
3. Remove `tailwind.config.js` (optional in v4) or migrate to CSS-based config.
|
|
177
|
+
|
|
178
|
+
4. Remove `postcss.config.js` if you have one (no longer needed).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeScript Vite Configuration for TailwindCSS v4
|
|
3
|
+
*
|
|
4
|
+
* This configuration file is automatically merged with the project's vite.config
|
|
5
|
+
* by NativeScript's Vite plugin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import nativescriptTailwind from "@nativescript/tailwind";
|
|
9
|
+
|
|
10
|
+
export default () => {
|
|
11
|
+
return {
|
|
12
|
+
plugins: [nativescriptTailwind()],
|
|
13
|
+
};
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ns-tailwind-vite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TailwindCSS v4 for NativeScript with Vite support",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./vite": "./src/vite-plugin.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"nativescript.vite.mjs"
|
|
14
|
+
],
|
|
15
|
+
"repository": "https://github.com/iammarjamal/ns-tailwind-vite",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nativescript",
|
|
18
|
+
"nativescript-vue",
|
|
19
|
+
"nativescript-theme",
|
|
20
|
+
"theme",
|
|
21
|
+
"tailwind",
|
|
22
|
+
"tailwindcss",
|
|
23
|
+
"styling",
|
|
24
|
+
"css",
|
|
25
|
+
"vite"
|
|
26
|
+
],
|
|
27
|
+
"author": "Ammar Jamal",
|
|
28
|
+
"contributors": [
|
|
29
|
+
{
|
|
30
|
+
"name": "Rqeim Team",
|
|
31
|
+
"email": "hi@rqeim.com"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"nativescript": {
|
|
35
|
+
"platforms": {
|
|
36
|
+
"android": "*",
|
|
37
|
+
"ios": "*"
|
|
38
|
+
},
|
|
39
|
+
"plugin": {
|
|
40
|
+
"nan": "true",
|
|
41
|
+
"pan": "true",
|
|
42
|
+
"core3": "true",
|
|
43
|
+
"vue": "true",
|
|
44
|
+
"category": "Developer"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"tailwindcss": "^4.0.0",
|
|
50
|
+
"vite": "^6.0.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@hookun/parse-animation-shorthand": "^0.1.4",
|
|
54
|
+
"@tailwindcss/vite": "^4.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"tailwindcss": "^4.0.0",
|
|
58
|
+
"vite": "^5.0.0 || ^6.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Transformer for NativeScript
|
|
3
|
+
*
|
|
4
|
+
* Transforms TailwindCSS v4 output to be compatible with NativeScript's CSS engine.
|
|
5
|
+
* This is a pure JavaScript implementation without PostCSS dependency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseSingle, serialize } from "@hookun/parse-animation-shorthand";
|
|
9
|
+
|
|
10
|
+
const remRE = /(\d*\.?\d+)\s*r?em/g;
|
|
11
|
+
|
|
12
|
+
// Supported CSS properties in NativeScript
|
|
13
|
+
const supportedProperties = {
|
|
14
|
+
"align-content": true,
|
|
15
|
+
"align-items": true,
|
|
16
|
+
"align-self": true,
|
|
17
|
+
"android-selected-tab-highlight-color": true,
|
|
18
|
+
"android-elevation": true,
|
|
19
|
+
"android-dynamic-elevation-offset": true,
|
|
20
|
+
animation: true,
|
|
21
|
+
"animation-delay": true,
|
|
22
|
+
"animation-direction": true,
|
|
23
|
+
"animation-duration": true,
|
|
24
|
+
"animation-fill-mode": true,
|
|
25
|
+
"animation-iteration-count": true,
|
|
26
|
+
"animation-name": true,
|
|
27
|
+
"animation-timing-function": true,
|
|
28
|
+
background: true,
|
|
29
|
+
"background-color": true,
|
|
30
|
+
"background-image": true,
|
|
31
|
+
"background-position": true,
|
|
32
|
+
"background-repeat": ["repeat", "repeat-x", "repeat-y", "no-repeat"],
|
|
33
|
+
"background-size": true,
|
|
34
|
+
"border-bottom-color": true,
|
|
35
|
+
"border-bottom-left-radius": true,
|
|
36
|
+
"border-bottom-right-radius": true,
|
|
37
|
+
"border-bottom-width": true,
|
|
38
|
+
"border-color": true,
|
|
39
|
+
"border-left-color": true,
|
|
40
|
+
"border-left-width": true,
|
|
41
|
+
"border-radius": true,
|
|
42
|
+
"border-right-color": true,
|
|
43
|
+
"border-right-width": true,
|
|
44
|
+
"border-top-color": true,
|
|
45
|
+
"border-top-left-radius": true,
|
|
46
|
+
"border-top-right-radius": true,
|
|
47
|
+
"border-top-width": true,
|
|
48
|
+
"border-width": true,
|
|
49
|
+
"box-shadow": true,
|
|
50
|
+
"clip-path": true,
|
|
51
|
+
color: true,
|
|
52
|
+
flex: true,
|
|
53
|
+
"flex-grow": true,
|
|
54
|
+
"flex-direction": true,
|
|
55
|
+
"flex-shrink": true,
|
|
56
|
+
"flex-wrap": true,
|
|
57
|
+
font: true,
|
|
58
|
+
"font-family": true,
|
|
59
|
+
"font-size": true,
|
|
60
|
+
"font-style": ["italic", "normal"],
|
|
61
|
+
"font-weight": true,
|
|
62
|
+
"font-variation-settings": true,
|
|
63
|
+
height: true,
|
|
64
|
+
"highlight-color": true,
|
|
65
|
+
"horizontal-align": ["left", "center", "right", "stretch"],
|
|
66
|
+
"justify-content": true,
|
|
67
|
+
"justify-items": true,
|
|
68
|
+
"justify-self": true,
|
|
69
|
+
"letter-spacing": true,
|
|
70
|
+
"line-height": true,
|
|
71
|
+
margin: true,
|
|
72
|
+
"margin-bottom": true,
|
|
73
|
+
"margin-left": true,
|
|
74
|
+
"margin-right": true,
|
|
75
|
+
"margin-top": true,
|
|
76
|
+
"margin-block": true,
|
|
77
|
+
"margin-block-start": true,
|
|
78
|
+
"margin-block-end": true,
|
|
79
|
+
"margin-inline": true,
|
|
80
|
+
"margin-inline-start": true,
|
|
81
|
+
"margin-inline-end": true,
|
|
82
|
+
"min-height": true,
|
|
83
|
+
"min-width": true,
|
|
84
|
+
"max-height": true,
|
|
85
|
+
"max-width": true,
|
|
86
|
+
"off-background-color": true,
|
|
87
|
+
opacity: true,
|
|
88
|
+
order: true,
|
|
89
|
+
padding: true,
|
|
90
|
+
"padding-block": true,
|
|
91
|
+
"padding-bottom": true,
|
|
92
|
+
"padding-inline": true,
|
|
93
|
+
"padding-left": true,
|
|
94
|
+
"padding-right": true,
|
|
95
|
+
"padding-top": true,
|
|
96
|
+
"place-content": true,
|
|
97
|
+
"placeholder-color": true,
|
|
98
|
+
"place-items": true,
|
|
99
|
+
"place-self": true,
|
|
100
|
+
"selected-tab-text-color": true,
|
|
101
|
+
"tab-background-color": true,
|
|
102
|
+
"tab-text-color": true,
|
|
103
|
+
"tab-text-font-size": true,
|
|
104
|
+
"text-transform": ["none", "capitalize", "uppercase", "lowercase"],
|
|
105
|
+
"text-align": ["left", "center", "right"],
|
|
106
|
+
"text-decoration": ["none", "line-through", "underline"],
|
|
107
|
+
"text-shadow": true,
|
|
108
|
+
transform: true,
|
|
109
|
+
rotate: true,
|
|
110
|
+
"vertical-align": ["top", "center", "bottom", "stretch"],
|
|
111
|
+
visibility: ["visible", "collapse"],
|
|
112
|
+
width: true,
|
|
113
|
+
"z-index": true,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const unsupportedPseudoSelectors = [":focus-within", ":hover"];
|
|
117
|
+
const unsupportedValues = ["max-content", "min-content", "vh", "vw"];
|
|
118
|
+
const unsupportedAtRules = ["@media", "@supports", "@property", "@keyframes"];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a CSS property is supported in NativeScript
|
|
122
|
+
*/
|
|
123
|
+
function isSupportedProperty(prop, val = null) {
|
|
124
|
+
const rules = supportedProperties[prop];
|
|
125
|
+
if (!rules) return false;
|
|
126
|
+
|
|
127
|
+
if (val) {
|
|
128
|
+
if (unsupportedValues.some((unit) => val.endsWith(unit))) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(rules)) {
|
|
133
|
+
return rules.includes(val);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a selector is supported in NativeScript
|
|
142
|
+
*/
|
|
143
|
+
function isSupportedSelector(selector) {
|
|
144
|
+
return !unsupportedPseudoSelectors.some((pseudo) =>
|
|
145
|
+
selector.includes(pseudo)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert camelCase to kebab-case
|
|
151
|
+
*/
|
|
152
|
+
function camelToKebab(input) {
|
|
153
|
+
return input.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Expand animation shorthand to individual properties
|
|
158
|
+
*/
|
|
159
|
+
function expandAnimation(value) {
|
|
160
|
+
try {
|
|
161
|
+
const styles = parseSingle(value);
|
|
162
|
+
if (styles.duration && Number.isInteger(styles.duration)) {
|
|
163
|
+
styles.duration = `${styles.duration / 1000}s`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Object.entries(styles)
|
|
167
|
+
.filter(([, v]) => typeof v === "object")
|
|
168
|
+
.forEach(([key, v]) => {
|
|
169
|
+
styles[key] = serialize({ [key]: v }).split(" ")[0];
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return Object.entries(styles)
|
|
173
|
+
.filter(([, v]) => v !== "unset")
|
|
174
|
+
.map(([key, v]) => `animation-${camelToKebab(key)}: ${v}`)
|
|
175
|
+
.join(";\n ");
|
|
176
|
+
} catch {
|
|
177
|
+
return `animation: ${value}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Transform a CSS value (convert rem/em to px equivalent)
|
|
183
|
+
*/
|
|
184
|
+
function transformValue(value, debug = false) {
|
|
185
|
+
if (!value) return value;
|
|
186
|
+
|
|
187
|
+
// Convert em/rem to device pixels (16px base)
|
|
188
|
+
if (value.includes("rem") || value.includes("em")) {
|
|
189
|
+
value = value.replace(remRE, (match, num) => {
|
|
190
|
+
const converted = String(parseFloat(num) * 16);
|
|
191
|
+
if (debug) {
|
|
192
|
+
console.log(
|
|
193
|
+
`[nativescript-tailwind] Converting ${match} to ${converted}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return converted;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Transform a single CSS declaration
|
|
205
|
+
*/
|
|
206
|
+
function transformDeclaration(prop, value, debug = false) {
|
|
207
|
+
// Skip CSS variables that define unsupported features
|
|
208
|
+
if (
|
|
209
|
+
[
|
|
210
|
+
"tw-ring",
|
|
211
|
+
"tw-shadow",
|
|
212
|
+
"tw-ordinal",
|
|
213
|
+
"tw-slashed-zero",
|
|
214
|
+
"tw-numeric",
|
|
215
|
+
].some((varName) => prop.startsWith(`--${varName}`))
|
|
216
|
+
) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Skip divide/space reverse variables in specific contexts
|
|
221
|
+
if (prop.match(/--tw-(divide|space)-[xy]-reverse/)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip color-mix values (not supported yet)
|
|
226
|
+
if (value?.includes("color-mix")) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Skip currentColor values
|
|
231
|
+
if (value?.includes("currentColor")) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Transform visibility: hidden -> collapse
|
|
236
|
+
if (prop === "visibility" && value === "hidden") {
|
|
237
|
+
return { prop, value: "collapse" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Transform vertical-align: middle -> center
|
|
241
|
+
if (prop === "vertical-align" && value === "middle") {
|
|
242
|
+
return { prop, value: "center" };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Expand animation shorthand
|
|
246
|
+
if (prop === "animation") {
|
|
247
|
+
return { prop: "__expanded__", value: expandAnimation(value) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Transform value (rem/em conversion)
|
|
251
|
+
const transformedValue = transformValue(value, debug);
|
|
252
|
+
|
|
253
|
+
// Check if property is supported
|
|
254
|
+
if (!prop.startsWith("--") && !isSupportedProperty(prop, transformedValue)) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { prop, value: transformedValue };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Transform a CSS selector
|
|
263
|
+
*/
|
|
264
|
+
function transformSelector(selector) {
|
|
265
|
+
// Replace :root and :host with NativeScript equivalents
|
|
266
|
+
const rootClasses = ".ns-root, .ns-modal";
|
|
267
|
+
selector = selector
|
|
268
|
+
.replace(/:root/g, rootClasses)
|
|
269
|
+
.replace(/:host/g, rootClasses);
|
|
270
|
+
|
|
271
|
+
// Remove :where() wrapper
|
|
272
|
+
selector = selector.replace(/:where\(([^)]+)\)/g, "$1");
|
|
273
|
+
|
|
274
|
+
// Transform space/divide selectors
|
|
275
|
+
selector = selector.replace(/:not\(:last-child\)/g, "* + *");
|
|
276
|
+
selector = selector.replace(
|
|
277
|
+
/:not\(\[hidden\]\) ~ :not\(\[hidden\]\)/g,
|
|
278
|
+
"* + *"
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Handle ::placeholder (convert to base selector for placeholder-color)
|
|
282
|
+
if (selector.includes("::placeholder")) {
|
|
283
|
+
selector = selector.replace(/::placeholder/g, "");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return selector.trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse CSS into rules and at-rules (simple parser)
|
|
291
|
+
*/
|
|
292
|
+
function parseCSS(css) {
|
|
293
|
+
const result = [];
|
|
294
|
+
let i = 0;
|
|
295
|
+
|
|
296
|
+
while (i < css.length) {
|
|
297
|
+
// Skip whitespace
|
|
298
|
+
while (i < css.length && /\s/.test(css[i])) i++;
|
|
299
|
+
if (i >= css.length) break;
|
|
300
|
+
|
|
301
|
+
// Check for at-rule
|
|
302
|
+
if (css[i] === "@") {
|
|
303
|
+
const atRuleMatch = css.slice(i).match(/^@(\w+)([^{;]*)(;|\{)/);
|
|
304
|
+
if (atRuleMatch) {
|
|
305
|
+
const atRuleName = atRuleMatch[1];
|
|
306
|
+
const atRuleParams = atRuleMatch[2].trim();
|
|
307
|
+
const atRuleEnd = atRuleMatch[3];
|
|
308
|
+
|
|
309
|
+
i += atRuleMatch[0].length;
|
|
310
|
+
|
|
311
|
+
if (atRuleEnd === "{") {
|
|
312
|
+
// Find matching closing brace
|
|
313
|
+
let braceCount = 1;
|
|
314
|
+
let bodyStart = i;
|
|
315
|
+
while (i < css.length && braceCount > 0) {
|
|
316
|
+
if (css[i] === "{") braceCount++;
|
|
317
|
+
if (css[i] === "}") braceCount--;
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
const body = css.slice(bodyStart, i - 1);
|
|
321
|
+
result.push({
|
|
322
|
+
type: "at-rule",
|
|
323
|
+
name: atRuleName,
|
|
324
|
+
params: atRuleParams,
|
|
325
|
+
body: body,
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
result.push({
|
|
329
|
+
type: "at-rule",
|
|
330
|
+
name: atRuleName,
|
|
331
|
+
params: atRuleParams,
|
|
332
|
+
body: null,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Parse rule (selector + declarations)
|
|
340
|
+
const selectorEnd = css.indexOf("{", i);
|
|
341
|
+
if (selectorEnd === -1) break;
|
|
342
|
+
|
|
343
|
+
const selector = css.slice(i, selectorEnd).trim();
|
|
344
|
+
i = selectorEnd + 1;
|
|
345
|
+
|
|
346
|
+
// Find closing brace
|
|
347
|
+
let braceCount = 1;
|
|
348
|
+
let bodyStart = i;
|
|
349
|
+
while (i < css.length && braceCount > 0) {
|
|
350
|
+
if (css[i] === "{") braceCount++;
|
|
351
|
+
if (css[i] === "}") braceCount--;
|
|
352
|
+
i++;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const body = css.slice(bodyStart, i - 1).trim();
|
|
356
|
+
|
|
357
|
+
if (selector && body) {
|
|
358
|
+
result.push({
|
|
359
|
+
type: "rule",
|
|
360
|
+
selector,
|
|
361
|
+
body,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Parse declarations from a rule body
|
|
371
|
+
*/
|
|
372
|
+
function parseDeclarations(body) {
|
|
373
|
+
const declarations = [];
|
|
374
|
+
const parts = body.split(";");
|
|
375
|
+
|
|
376
|
+
for (const part of parts) {
|
|
377
|
+
const trimmed = part.trim();
|
|
378
|
+
if (!trimmed) continue;
|
|
379
|
+
|
|
380
|
+
const colonIndex = trimmed.indexOf(":");
|
|
381
|
+
if (colonIndex === -1) continue;
|
|
382
|
+
|
|
383
|
+
const prop = trimmed.slice(0, colonIndex).trim();
|
|
384
|
+
const value = trimmed.slice(colonIndex + 1).trim();
|
|
385
|
+
|
|
386
|
+
if (prop && value) {
|
|
387
|
+
declarations.push({ prop, value });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return declarations;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Main CSS transformation function
|
|
396
|
+
*/
|
|
397
|
+
export function transformNativeScriptCSS(css, options = {}) {
|
|
398
|
+
const { debug = false } = options;
|
|
399
|
+
const rules = parseCSS(css);
|
|
400
|
+
const output = [];
|
|
401
|
+
|
|
402
|
+
for (const rule of rules) {
|
|
403
|
+
if (rule.type === "at-rule") {
|
|
404
|
+
// Handle @layer - flatten by processing its contents
|
|
405
|
+
if (rule.name === "layer") {
|
|
406
|
+
if (rule.body) {
|
|
407
|
+
const nested = transformNativeScriptCSS(rule.body, options);
|
|
408
|
+
output.push(nested);
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Handle @keyframes - keep as-is (supported in NativeScript)
|
|
414
|
+
if (rule.name === "keyframes") {
|
|
415
|
+
output.push(`@keyframes ${rule.params} {\n${rule.body}\n}`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Skip unsupported at-rules (@media, @supports, @property)
|
|
420
|
+
if (["media", "supports", "property"].includes(rule.name)) {
|
|
421
|
+
if (debug) {
|
|
422
|
+
console.log(`[nativescript-tailwind] Skipping @${rule.name}`);
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (rule.type === "rule") {
|
|
431
|
+
// Skip rules with CSS nesting (& selector)
|
|
432
|
+
if (rule.selector.includes("&")) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check if selector is supported
|
|
437
|
+
if (!isSupportedSelector(rule.selector)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Transform selector
|
|
442
|
+
let selector = transformSelector(rule.selector);
|
|
443
|
+
if (!selector) continue;
|
|
444
|
+
|
|
445
|
+
// Handle ::placeholder conversion
|
|
446
|
+
const isPlaceholder = rule.selector.includes("::placeholder");
|
|
447
|
+
|
|
448
|
+
// Parse and transform declarations
|
|
449
|
+
const declarations = parseDeclarations(rule.body);
|
|
450
|
+
const transformedDecls = [];
|
|
451
|
+
|
|
452
|
+
for (const { prop, value } of declarations) {
|
|
453
|
+
// For placeholder rules, convert color to placeholder-color
|
|
454
|
+
let actualProp = prop;
|
|
455
|
+
if (isPlaceholder && prop === "color") {
|
|
456
|
+
actualProp = "placeholder-color";
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const transformed = transformDeclaration(actualProp, value, debug);
|
|
460
|
+
if (transformed) {
|
|
461
|
+
if (transformed.prop === "__expanded__") {
|
|
462
|
+
// Expanded animation properties
|
|
463
|
+
transformedDecls.push(transformed.value);
|
|
464
|
+
} else {
|
|
465
|
+
transformedDecls.push(`${transformed.prop}: ${transformed.value}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (transformedDecls.length > 0) {
|
|
471
|
+
output.push(`${selector} {\n ${transformedDecls.join(";\n ")};\n}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return output.join("\n\n");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export default transformNativeScriptCSS;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nativescript/tailwind - TailwindCSS v4 plugin for NativeScript with Vite
|
|
3
|
+
*
|
|
4
|
+
* Main entry point that exports the Vite plugin for NativeScript CSS transformation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { default as nativescriptTailwind } from "./vite-plugin.js";
|
|
8
|
+
export { default } from "./vite-plugin.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nativescript/tailwind - Vite Plugin for TailwindCSS v4 + NativeScript
|
|
3
|
+
*
|
|
4
|
+
* This plugin wraps @tailwindcss/vite and transforms the CSS output
|
|
5
|
+
* to be compatible with NativeScript's CSS engine.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Full HMR support via @tailwindcss/vite
|
|
9
|
+
* - Automatic CSS transformation for NativeScript compatibility
|
|
10
|
+
* - rem/em to px conversion
|
|
11
|
+
* - Removal of unsupported CSS features (@media, @supports, etc.)
|
|
12
|
+
* - Animation shorthand expansion
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```js
|
|
16
|
+
* // vite.config.js
|
|
17
|
+
* import nativescriptTailwind from '@nativescript/tailwind/vite';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({
|
|
20
|
+
* plugins: [nativescriptTailwind()]
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Or auto-configured via nativescript.vite.mjs (recommended)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
28
|
+
import { transformNativeScriptCSS } from "./css-transformer.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates the NativeScript Tailwind Vite plugin
|
|
32
|
+
* @param {Object} options - Plugin options
|
|
33
|
+
* @param {boolean} options.debug - Enable debug logging
|
|
34
|
+
* @param {boolean} options.includeTailwind - Include @tailwindcss/vite plugin (default: true)
|
|
35
|
+
* @returns {import('vite').Plugin[]} Array of Vite plugins
|
|
36
|
+
*/
|
|
37
|
+
export default function nativescriptTailwind(options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
debug = process.env.VITE_DEBUG_LOGS === "1",
|
|
40
|
+
includeTailwind = true,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
const plugins = [];
|
|
44
|
+
|
|
45
|
+
// Include the official TailwindCSS v4 Vite plugin (optional)
|
|
46
|
+
if (includeTailwind) {
|
|
47
|
+
plugins.push(...tailwindcss());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// NativeScript CSS transformation plugin
|
|
51
|
+
plugins.push({
|
|
52
|
+
name: "nativescript-tailwind",
|
|
53
|
+
enforce: "post",
|
|
54
|
+
|
|
55
|
+
// Transform CSS files for NativeScript compatibility
|
|
56
|
+
transform(code, id) {
|
|
57
|
+
// Only process CSS files and Vue style blocks
|
|
58
|
+
const isCSSFile = id.endsWith(".css");
|
|
59
|
+
const isVueStyle =
|
|
60
|
+
id.includes("?vue&type=style") || id.includes("&lang.css");
|
|
61
|
+
const isSCSS = id.includes("scss") || id.includes("sass");
|
|
62
|
+
|
|
63
|
+
if (!isCSSFile && !isVueStyle) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Skip SCSS/SASS files (they need preprocessing first)
|
|
68
|
+
if (isSCSS) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Skip if no CSS content
|
|
73
|
+
if (!code || typeof code !== "string") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Skip node_modules except for tailwind
|
|
78
|
+
if (id.includes("node_modules") && !id.includes("tailwindcss")) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const transformed = transformNativeScriptCSS(code, { debug });
|
|
84
|
+
|
|
85
|
+
if (debug) {
|
|
86
|
+
console.log(`[nativescript-tailwind] Transformed: ${id}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
code: transformed,
|
|
91
|
+
map: null, // Source maps not needed for CSS in NativeScript
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(
|
|
95
|
+
`[nativescript-tailwind] Error transforming ${id}:`,
|
|
96
|
+
error
|
|
97
|
+
);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Handle HMR for CSS updates
|
|
103
|
+
handleHotUpdate({ file, server, modules }) {
|
|
104
|
+
if (file.endsWith(".css")) {
|
|
105
|
+
if (debug) {
|
|
106
|
+
console.log(`[nativescript-tailwind] HMR update: ${file}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Let Vite handle the HMR normally
|
|
110
|
+
return modules;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return plugins;
|
|
115
|
+
}
|