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 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
+ ![Tailwind CSS is awesome!](https://user-images.githubusercontent.com/879060/81098285-73e3ad80-8f09-11ea-8cfa-7e2ec2eebcde.png)
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
+ }