@nifrajs/cli 2.14.1 → 3.2.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.
Files changed (203) hide show
  1. package/README.md +10 -0
  2. package/dist/assure-hydration.d.ts.map +1 -1
  3. package/dist/assure-hydration.js +6 -1
  4. package/dist/assure-hydration.js.map +1 -1
  5. package/dist/assure.d.ts.map +1 -1
  6. package/dist/assure.js +3 -2
  7. package/dist/assure.js.map +1 -1
  8. package/dist/check-diagnostics.d.ts +165 -0
  9. package/dist/check-diagnostics.d.ts.map +1 -0
  10. package/dist/check-diagnostics.js +1009 -0
  11. package/dist/check-diagnostics.js.map +1 -0
  12. package/dist/check-scan.d.ts +245 -0
  13. package/dist/check-scan.d.ts.map +1 -0
  14. package/dist/check-scan.js +1599 -0
  15. package/dist/check-scan.js.map +1 -0
  16. package/dist/check.d.ts +4 -324
  17. package/dist/check.d.ts.map +1 -1
  18. package/dist/check.js +8 -2346
  19. package/dist/check.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +285 -10
  22. package/dist/cli.js.map +1 -1
  23. package/dist/command-catalog.d.ts +27 -1
  24. package/dist/command-catalog.d.ts.map +1 -1
  25. package/dist/command-catalog.js +142 -2
  26. package/dist/command-catalog.js.map +1 -1
  27. package/dist/doctor.d.ts +42 -0
  28. package/dist/doctor.d.ts.map +1 -1
  29. package/dist/doctor.js +64 -7
  30. package/dist/doctor.js.map +1 -1
  31. package/dist/env-file.d.ts +17 -0
  32. package/dist/env-file.d.ts.map +1 -0
  33. package/dist/env-file.js +113 -0
  34. package/dist/env-file.js.map +1 -0
  35. package/dist/fix-recipes.d.ts.map +1 -1
  36. package/dist/fix-recipes.js +91 -15
  37. package/dist/fix-recipes.js.map +1 -1
  38. package/dist/frontend-guidance.d.ts +57 -0
  39. package/dist/frontend-guidance.d.ts.map +1 -0
  40. package/dist/frontend-guidance.js +195 -0
  41. package/dist/frontend-guidance.js.map +1 -0
  42. package/dist/internal/reserved-segment-codemod.d.ts +36 -0
  43. package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
  44. package/dist/internal/reserved-segment-codemod.js +134 -0
  45. package/dist/internal/reserved-segment-codemod.js.map +1 -0
  46. package/dist/internal/typescript-import.d.ts +1 -1
  47. package/dist/internal/typescript-import.d.ts.map +1 -1
  48. package/dist/internal/typescript-import.js +62 -11
  49. package/dist/internal/typescript-import.js.map +1 -1
  50. package/dist/introspect.d.ts +26 -1
  51. package/dist/introspect.d.ts.map +1 -1
  52. package/dist/introspect.js +74 -24
  53. package/dist/introspect.js.map +1 -1
  54. package/dist/levels-tool.d.ts.map +1 -1
  55. package/dist/levels-tool.js +6 -0
  56. package/dist/levels-tool.js.map +1 -1
  57. package/dist/load.d.ts.map +1 -1
  58. package/dist/load.js +4 -1
  59. package/dist/load.js.map +1 -1
  60. package/dist/mcp-context.d.ts +48 -0
  61. package/dist/mcp-context.d.ts.map +1 -0
  62. package/dist/mcp-context.js +215 -0
  63. package/dist/mcp-context.js.map +1 -0
  64. package/dist/mcp-docs-tools.d.ts.map +1 -1
  65. package/dist/mcp-docs-tools.js +31 -0
  66. package/dist/mcp-docs-tools.js.map +1 -1
  67. package/dist/mcp-exec.d.ts +74 -0
  68. package/dist/mcp-exec.d.ts.map +1 -0
  69. package/dist/mcp-exec.js +844 -0
  70. package/dist/mcp-exec.js.map +1 -0
  71. package/dist/mcp-http.js +1 -1
  72. package/dist/mcp-http.js.map +1 -1
  73. package/dist/mcp-io.d.ts +26 -0
  74. package/dist/mcp-io.d.ts.map +1 -0
  75. package/dist/mcp-io.js +100 -0
  76. package/dist/mcp-io.js.map +1 -0
  77. package/dist/mcp-reflect.d.ts +16 -0
  78. package/dist/mcp-reflect.d.ts.map +1 -0
  79. package/dist/mcp-reflect.js +101 -0
  80. package/dist/mcp-reflect.js.map +1 -0
  81. package/dist/mcp-root.d.ts +7 -0
  82. package/dist/mcp-root.d.ts.map +1 -1
  83. package/dist/mcp-root.js +51 -3
  84. package/dist/mcp-root.js.map +1 -1
  85. package/dist/mcp.d.ts +10 -84
  86. package/dist/mcp.d.ts.map +1 -1
  87. package/dist/mcp.js +13 -1170
  88. package/dist/mcp.js.map +1 -1
  89. package/dist/openapi-tool.d.ts +5 -10
  90. package/dist/openapi-tool.d.ts.map +1 -1
  91. package/dist/openapi-tool.js +21 -2
  92. package/dist/openapi-tool.js.map +1 -1
  93. package/dist/openapi-types.d.ts +21 -0
  94. package/dist/openapi-types.d.ts.map +1 -0
  95. package/dist/openapi-types.js +214 -0
  96. package/dist/openapi-types.js.map +1 -0
  97. package/dist/port.d.ts.map +1 -1
  98. package/dist/port.js +2 -1
  99. package/dist/port.js.map +1 -1
  100. package/dist/project-facts.d.ts +2 -1
  101. package/dist/project-facts.d.ts.map +1 -1
  102. package/dist/project-facts.js.map +1 -1
  103. package/dist/recipes/3.0.0.d.ts +10 -0
  104. package/dist/recipes/3.0.0.d.ts.map +1 -0
  105. package/dist/recipes/3.0.0.js +23 -0
  106. package/dist/recipes/3.0.0.js.map +1 -0
  107. package/dist/recipes/index.d.ts.map +1 -1
  108. package/dist/recipes/index.js +2 -0
  109. package/dist/recipes/index.js.map +1 -1
  110. package/dist/release-verification.d.ts +4 -1
  111. package/dist/release-verification.d.ts.map +1 -1
  112. package/dist/release-verification.js +6 -123
  113. package/dist/release-verification.js.map +1 -1
  114. package/dist/rules/codes.d.ts +4 -0
  115. package/dist/rules/codes.d.ts.map +1 -1
  116. package/dist/rules/codes.js +4 -0
  117. package/dist/rules/codes.js.map +1 -1
  118. package/dist/rules/islands.d.ts +4 -0
  119. package/dist/rules/islands.d.ts.map +1 -0
  120. package/dist/rules/islands.js +155 -0
  121. package/dist/rules/islands.js.map +1 -0
  122. package/dist/rules/nano.d.ts +6 -0
  123. package/dist/rules/nano.d.ts.map +1 -0
  124. package/dist/rules/nano.js +264 -0
  125. package/dist/rules/nano.js.map +1 -0
  126. package/dist/rules/routes.d.ts.map +1 -1
  127. package/dist/rules/routes.js +17 -30
  128. package/dist/rules/routes.js.map +1 -1
  129. package/dist/rules/security.d.ts.map +1 -1
  130. package/dist/rules/security.js +38 -4
  131. package/dist/rules/security.js.map +1 -1
  132. package/dist/scaffold.d.ts +12 -6
  133. package/dist/scaffold.d.ts.map +1 -1
  134. package/dist/scaffold.js +149 -10
  135. package/dist/scaffold.js.map +1 -1
  136. package/dist/search-terms.d.ts.map +1 -1
  137. package/dist/search-terms.js +5 -3
  138. package/dist/search-terms.js.map +1 -1
  139. package/dist/stylex-migrate.d.ts +38 -0
  140. package/dist/stylex-migrate.d.ts.map +1 -0
  141. package/dist/stylex-migrate.js +1052 -0
  142. package/dist/stylex-migrate.js.map +1 -0
  143. package/dist/sync-manifest.d.ts.map +1 -1
  144. package/dist/sync-manifest.js +2 -1
  145. package/dist/sync-manifest.js.map +1 -1
  146. package/dist/verification-plan.d.ts +24 -0
  147. package/dist/verification-plan.d.ts.map +1 -0
  148. package/dist/verification-plan.js +116 -0
  149. package/dist/verification-plan.js.map +1 -0
  150. package/dist/work-graph.d.ts.map +1 -1
  151. package/dist/work-graph.js +6 -3
  152. package/dist/work-graph.js.map +1 -1
  153. package/dist/workspace-link.d.ts +37 -0
  154. package/dist/workspace-link.d.ts.map +1 -0
  155. package/dist/workspace-link.js +72 -0
  156. package/dist/workspace-link.js.map +1 -0
  157. package/docs/examples.json +37 -1
  158. package/docs/llms-full.txt +1598 -44
  159. package/docs/types.json +5157 -172
  160. package/package.json +9 -9
  161. package/src/assure-hydration.ts +6 -1
  162. package/src/assure.ts +4 -2
  163. package/src/check-diagnostics.ts +1219 -0
  164. package/src/check-scan.ts +1825 -0
  165. package/src/check.ts +47 -2731
  166. package/src/cli.ts +291 -10
  167. package/src/command-catalog.ts +177 -2
  168. package/src/doctor.ts +99 -8
  169. package/src/env-file.ts +121 -0
  170. package/src/fix-recipes.ts +101 -15
  171. package/src/frontend-guidance.ts +259 -0
  172. package/src/internal/reserved-segment-codemod.ts +150 -0
  173. package/src/internal/typescript-import.ts +61 -11
  174. package/src/introspect.ts +82 -21
  175. package/src/levels-tool.ts +6 -0
  176. package/src/load.ts +4 -1
  177. package/src/mcp-context.ts +266 -0
  178. package/src/mcp-docs-tools.ts +37 -0
  179. package/src/mcp-exec.ts +1040 -0
  180. package/src/mcp-http.ts +1 -1
  181. package/src/mcp-io.ts +118 -0
  182. package/src/mcp-reflect.ts +140 -0
  183. package/src/mcp-root.ts +62 -4
  184. package/src/mcp.ts +61 -1436
  185. package/src/openapi-tool.ts +32 -2
  186. package/src/openapi-types.ts +261 -0
  187. package/src/port.ts +2 -1
  188. package/src/project-facts.ts +2 -4
  189. package/src/recipes/3.0.0.ts +24 -0
  190. package/src/recipes/index.ts +2 -0
  191. package/src/release-verification.ts +22 -147
  192. package/src/rules/codes.ts +4 -0
  193. package/src/rules/islands.ts +171 -0
  194. package/src/rules/nano.ts +295 -0
  195. package/src/rules/routes.ts +17 -30
  196. package/src/rules/security.ts +40 -4
  197. package/src/scaffold.ts +169 -9
  198. package/src/search-terms.ts +7 -3
  199. package/src/stylex-migrate.ts +1139 -0
  200. package/src/sync-manifest.ts +2 -1
  201. package/src/verification-plan.ts +242 -0
  202. package/src/work-graph.ts +6 -3
  203. package/src/workspace-link.ts +80 -0
@@ -0,0 +1,1139 @@
1
+ /**
2
+ * Safe, source-preserving Tailwind utility migration for StyleX.
3
+ *
4
+ * This is intentionally a bounded codemod rather than a Tailwind interpreter. Static utilities with
5
+ * a direct StyleX equivalent are rewritten; dynamic className expressions, parent-dependent variants,
6
+ * arbitrary values, and utilities outside the table are left untouched and reported.
7
+ */
8
+ import { readdir, readFile, writeFile } from "node:fs/promises"
9
+ import { relative, resolve } from "node:path"
10
+
11
+ export interface MigrationIssue {
12
+ readonly file: string
13
+ readonly line: number
14
+ readonly token: string
15
+ readonly reason: string
16
+ }
17
+
18
+ export interface SourceMigration {
19
+ readonly source: string
20
+ readonly changed: boolean
21
+ readonly replacements: number
22
+ readonly issues: readonly MigrationIssue[]
23
+ }
24
+
25
+ export interface StylexMigrationOptions {
26
+ readonly write?: boolean
27
+ }
28
+
29
+ export interface StylexMigrationFile {
30
+ readonly file: string
31
+ readonly changed: boolean
32
+ readonly replacements: number
33
+ readonly written: boolean
34
+ readonly issues: readonly MigrationIssue[]
35
+ }
36
+
37
+ export interface StylexMigrationResult {
38
+ readonly ok: boolean
39
+ readonly from: "tailwind"
40
+ readonly to: "stylex"
41
+ readonly write: boolean
42
+ readonly scanned: number
43
+ readonly changed: readonly string[]
44
+ readonly written: readonly string[]
45
+ readonly issues: readonly MigrationIssue[]
46
+ readonly files: readonly StylexMigrationFile[]
47
+ }
48
+
49
+ interface StyleObject {
50
+ readonly [key: string]: string | number | StyleObject
51
+ }
52
+
53
+ interface UtilityConversion {
54
+ readonly style?: StyleObject
55
+ readonly reason?: string
56
+ }
57
+
58
+ interface ClassAttribute {
59
+ readonly start: number
60
+ readonly end: number
61
+ readonly value?: string
62
+ readonly expression?: string
63
+ }
64
+
65
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/
66
+ const SKIP_DIRECTORIES = new Set([
67
+ ".git",
68
+ ".next",
69
+ ".nuxt",
70
+ ".planning",
71
+ ".turbo",
72
+ "build",
73
+ "coverage",
74
+ "dist",
75
+ "node_modules",
76
+ "out",
77
+ "public",
78
+ "vendor",
79
+ ])
80
+
81
+ const BREAKPOINTS: Readonly<Record<string, string>> = {
82
+ sm: "640px",
83
+ md: "768px",
84
+ lg: "1024px",
85
+ xl: "1280px",
86
+ "2xl": "1536px",
87
+ }
88
+
89
+ const SPACING: Readonly<Record<string, string | number>> = {
90
+ "0": 0,
91
+ px: "1px",
92
+ "0.5": "0.125rem",
93
+ "1": "0.25rem",
94
+ "1.5": "0.375rem",
95
+ "2": "0.5rem",
96
+ "2.5": "0.625rem",
97
+ "3": "0.75rem",
98
+ "3.5": "0.875rem",
99
+ "4": "1rem",
100
+ "5": "1.25rem",
101
+ "6": "1.5rem",
102
+ "7": "1.75rem",
103
+ "8": "2rem",
104
+ "9": "2.25rem",
105
+ "10": "2.5rem",
106
+ "11": "2.75rem",
107
+ "12": "3rem",
108
+ "14": "3.5rem",
109
+ "16": "4rem",
110
+ "20": "5rem",
111
+ "24": "6rem",
112
+ "28": "7rem",
113
+ "32": "8rem",
114
+ "36": "9rem",
115
+ "40": "10rem",
116
+ "44": "11rem",
117
+ "48": "12rem",
118
+ "52": "13rem",
119
+ "56": "14rem",
120
+ "60": "15rem",
121
+ "64": "16rem",
122
+ "72": "18rem",
123
+ "80": "20rem",
124
+ "96": "24rem",
125
+ }
126
+
127
+ const FRACTIONS: Readonly<Record<string, string>> = {
128
+ "1/2": "50%",
129
+ "1/3": "33.333333%",
130
+ "2/3": "66.666667%",
131
+ "1/4": "25%",
132
+ "2/4": "50%",
133
+ "3/4": "75%",
134
+ "1/5": "20%",
135
+ "2/5": "40%",
136
+ "3/5": "60%",
137
+ "4/5": "80%",
138
+ "1/6": "16.666667%",
139
+ "2/6": "33.333333%",
140
+ "3/6": "50%",
141
+ "4/6": "66.666667%",
142
+ "5/6": "83.333333%",
143
+ "1/12": "8.333333%",
144
+ "2/12": "16.666667%",
145
+ "3/12": "25%",
146
+ "4/12": "33.333333%",
147
+ "5/12": "41.666667%",
148
+ "6/12": "50%",
149
+ "7/12": "58.333333%",
150
+ "8/12": "66.666667%",
151
+ "9/12": "75%",
152
+ "10/12": "83.333333%",
153
+ "11/12": "91.666667%",
154
+ }
155
+
156
+ const COLORS: Readonly<Record<string, string>> = {
157
+ transparent: "transparent",
158
+ current: "currentColor",
159
+ inherit: "inherit",
160
+ black: "#000000",
161
+ white: "#ffffff",
162
+ "slate-50": "#f8fafc",
163
+ "slate-100": "#f1f5f9",
164
+ "slate-200": "#e2e8f0",
165
+ "slate-300": "#cbd5e1",
166
+ "slate-400": "#94a3b8",
167
+ "slate-500": "#64748b",
168
+ "slate-600": "#475569",
169
+ "slate-700": "#334155",
170
+ "slate-800": "#1e293b",
171
+ "slate-900": "#0f172a",
172
+ "slate-950": "#020617",
173
+ "gray-50": "#f9fafb",
174
+ "gray-100": "#f3f4f6",
175
+ "gray-200": "#e5e7eb",
176
+ "gray-300": "#d1d5db",
177
+ "gray-400": "#9ca3af",
178
+ "gray-500": "#6b7280",
179
+ "gray-600": "#4b5563",
180
+ "gray-700": "#374151",
181
+ "gray-800": "#1f2937",
182
+ "gray-900": "#111827",
183
+ "gray-950": "#030712",
184
+ "red-50": "#fef2f2",
185
+ "red-100": "#fee2e2",
186
+ "red-200": "#fecaca",
187
+ "red-300": "#fca5a5",
188
+ "red-400": "#f87171",
189
+ "red-500": "#ef4444",
190
+ "red-600": "#dc2626",
191
+ "red-700": "#b91c1c",
192
+ "red-800": "#991b1b",
193
+ "red-900": "#7f1d1d",
194
+ "red-950": "#450a0a",
195
+ "orange-50": "#fff7ed",
196
+ "orange-100": "#ffedd5",
197
+ "orange-200": "#fed7aa",
198
+ "orange-300": "#fdba74",
199
+ "orange-400": "#fb923c",
200
+ "orange-500": "#f97316",
201
+ "orange-600": "#ea580c",
202
+ "orange-700": "#c2410c",
203
+ "orange-800": "#9a3412",
204
+ "orange-900": "#7c2d12",
205
+ "orange-950": "#431407",
206
+ "amber-50": "#fffbeb",
207
+ "amber-100": "#fef3c7",
208
+ "amber-200": "#fde68a",
209
+ "amber-300": "#fcd34d",
210
+ "amber-400": "#fbbf24",
211
+ "amber-500": "#f59e0b",
212
+ "amber-600": "#d97706",
213
+ "amber-700": "#b45309",
214
+ "amber-800": "#92400e",
215
+ "amber-900": "#78350f",
216
+ "amber-950": "#451a03",
217
+ "yellow-50": "#fefce8",
218
+ "yellow-100": "#fef9c3",
219
+ "yellow-200": "#fef08a",
220
+ "yellow-300": "#fde047",
221
+ "yellow-400": "#facc15",
222
+ "yellow-500": "#eab308",
223
+ "yellow-600": "#ca8a04",
224
+ "yellow-700": "#a16207",
225
+ "yellow-800": "#854d0e",
226
+ "yellow-900": "#713f12",
227
+ "yellow-950": "#422006",
228
+ "green-50": "#f0fdf4",
229
+ "green-100": "#dcfce7",
230
+ "green-200": "#bbf7d0",
231
+ "green-300": "#86efac",
232
+ "green-400": "#4ade80",
233
+ "green-500": "#22c55e",
234
+ "green-600": "#16a34a",
235
+ "green-700": "#15803d",
236
+ "green-800": "#166534",
237
+ "green-900": "#14532d",
238
+ "green-950": "#052e16",
239
+ "emerald-50": "#ecfdf5",
240
+ "emerald-100": "#d1fae5",
241
+ "emerald-200": "#a7f3d0",
242
+ "emerald-300": "#6ee7b7",
243
+ "emerald-400": "#34d399",
244
+ "emerald-500": "#10b981",
245
+ "emerald-600": "#059669",
246
+ "emerald-700": "#047857",
247
+ "emerald-800": "#065f46",
248
+ "emerald-900": "#064e3b",
249
+ "emerald-950": "#022c22",
250
+ "teal-50": "#f0fdfa",
251
+ "teal-100": "#ccfbf1",
252
+ "teal-200": "#99f6e4",
253
+ "teal-300": "#5eead4",
254
+ "teal-400": "#2dd4bf",
255
+ "teal-500": "#14b8a6",
256
+ "teal-600": "#0d9488",
257
+ "teal-700": "#0f766e",
258
+ "teal-800": "#115e59",
259
+ "teal-900": "#134e4a",
260
+ "teal-950": "#042f2e",
261
+ "cyan-50": "#ecfeff",
262
+ "cyan-100": "#cffafe",
263
+ "cyan-200": "#a5f3fc",
264
+ "cyan-300": "#67e8f9",
265
+ "cyan-400": "#22d3ee",
266
+ "cyan-500": "#06b6d4",
267
+ "cyan-600": "#0891b2",
268
+ "cyan-700": "#0e7490",
269
+ "cyan-800": "#155e75",
270
+ "cyan-900": "#164e63",
271
+ "cyan-950": "#083344",
272
+ "blue-50": "#eff6ff",
273
+ "blue-100": "#dbeafe",
274
+ "blue-200": "#bfdbfe",
275
+ "blue-300": "#93c5fd",
276
+ "blue-400": "#60a5fa",
277
+ "blue-500": "#3b82f6",
278
+ "blue-600": "#2563eb",
279
+ "blue-700": "#1d4ed8",
280
+ "blue-800": "#1e40af",
281
+ "blue-900": "#1e3a8a",
282
+ "blue-950": "#172554",
283
+ "indigo-50": "#eef2ff",
284
+ "indigo-100": "#e0e7ff",
285
+ "indigo-200": "#c7d2fe",
286
+ "indigo-300": "#a5b4fc",
287
+ "indigo-400": "#818cf8",
288
+ "indigo-500": "#6366f1",
289
+ "indigo-600": "#4f46e5",
290
+ "indigo-700": "#4338ca",
291
+ "indigo-800": "#3730a3",
292
+ "indigo-900": "#312e81",
293
+ "indigo-950": "#1e1b4b",
294
+ "violet-50": "#f5f3ff",
295
+ "violet-100": "#ede9fe",
296
+ "violet-200": "#ddd6fe",
297
+ "violet-300": "#c4b5fd",
298
+ "violet-400": "#a78bfa",
299
+ "violet-500": "#8b5cf6",
300
+ "violet-600": "#7c3aed",
301
+ "violet-700": "#6d28d9",
302
+ "violet-800": "#5b21b6",
303
+ "violet-900": "#4c1d95",
304
+ "violet-950": "#2e1065",
305
+ "purple-50": "#faf5ff",
306
+ "purple-100": "#f3e8ff",
307
+ "purple-200": "#e9d5ff",
308
+ "purple-300": "#d8b4fe",
309
+ "purple-400": "#c084fc",
310
+ "purple-500": "#a855f7",
311
+ "purple-600": "#9333ea",
312
+ "purple-700": "#7e22ce",
313
+ "purple-800": "#6b21a8",
314
+ "purple-900": "#581c87",
315
+ "purple-950": "#3b0764",
316
+ "pink-50": "#fdf2f8",
317
+ "pink-100": "#fce7f3",
318
+ "pink-200": "#fbcfe8",
319
+ "pink-300": "#f9a8d4",
320
+ "pink-400": "#f472b6",
321
+ "pink-500": "#ec4899",
322
+ "pink-600": "#db2777",
323
+ "pink-700": "#be185d",
324
+ "pink-800": "#9d174d",
325
+ "pink-900": "#831843",
326
+ "pink-950": "#500724",
327
+ "rose-50": "#fff1f2",
328
+ "rose-100": "#ffe4e6",
329
+ "rose-200": "#fecdd3",
330
+ "rose-300": "#fda4af",
331
+ "rose-400": "#fb7185",
332
+ "rose-500": "#f43f5e",
333
+ "rose-600": "#e11d48",
334
+ "rose-700": "#be123c",
335
+ "rose-800": "#9f1239",
336
+ "rose-900": "#881337",
337
+ "rose-950": "#4c0519",
338
+ }
339
+
340
+ const STATIC_STYLES: Readonly<Record<string, StyleObject>> = {
341
+ block: { display: "block" },
342
+ "inline-block": { display: "inline-block" },
343
+ inline: { display: "inline" },
344
+ flex: { display: "flex" },
345
+ "inline-flex": { display: "inline-flex" },
346
+ grid: { display: "grid" },
347
+ "inline-grid": { display: "inline-grid" },
348
+ contents: { display: "contents" },
349
+ "flow-root": { display: "flow-root" },
350
+ hidden: { display: "none" },
351
+ static: { position: "static" },
352
+ fixed: { position: "fixed" },
353
+ absolute: { position: "absolute" },
354
+ relative: { position: "relative" },
355
+ sticky: { position: "sticky" },
356
+ "flex-row": { flexDirection: "row" },
357
+ "flex-row-reverse": { flexDirection: "row-reverse" },
358
+ "flex-col": { flexDirection: "column" },
359
+ "flex-col-reverse": { flexDirection: "column-reverse" },
360
+ "flex-wrap": { flexWrap: "wrap" },
361
+ "flex-wrap-reverse": { flexWrap: "wrap-reverse" },
362
+ "flex-nowrap": { flexWrap: "nowrap" },
363
+ "flex-1": { flex: "1 1 0%" },
364
+ "flex-auto": { flex: "1 1 auto" },
365
+ "flex-initial": { flex: "0 1 auto" },
366
+ "flex-none": { flex: "none" },
367
+ grow: { flexGrow: 1 },
368
+ "grow-0": { flexGrow: 0 },
369
+ shrink: { flexShrink: 1 },
370
+ "shrink-0": { flexShrink: 0 },
371
+ "justify-normal": { justifyContent: "normal" },
372
+ "justify-start": { justifyContent: "flex-start" },
373
+ "justify-end": { justifyContent: "flex-end" },
374
+ "justify-center": { justifyContent: "center" },
375
+ "justify-between": { justifyContent: "space-between" },
376
+ "justify-around": { justifyContent: "space-around" },
377
+ "justify-evenly": { justifyContent: "space-evenly" },
378
+ "justify-stretch": { justifyContent: "stretch" },
379
+ "items-start": { alignItems: "flex-start" },
380
+ "items-end": { alignItems: "flex-end" },
381
+ "items-center": { alignItems: "center" },
382
+ "items-baseline": { alignItems: "baseline" },
383
+ "items-stretch": { alignItems: "stretch" },
384
+ "self-auto": { alignSelf: "auto" },
385
+ "self-start": { alignSelf: "flex-start" },
386
+ "self-end": { alignSelf: "flex-end" },
387
+ "self-center": { alignSelf: "center" },
388
+ "self-stretch": { alignSelf: "stretch" },
389
+ "place-items-start": { placeItems: "start" },
390
+ "place-items-end": { placeItems: "end" },
391
+ "place-items-center": { placeItems: "center" },
392
+ "place-items-stretch": { placeItems: "stretch" },
393
+ "place-content-start": { placeContent: "start" },
394
+ "place-content-end": { placeContent: "end" },
395
+ "place-content-center": { placeContent: "center" },
396
+ "place-content-between": { placeContent: "space-between" },
397
+ "place-content-around": { placeContent: "space-around" },
398
+ "place-content-evenly": { placeContent: "space-evenly" },
399
+ "place-content-stretch": { placeContent: "stretch" },
400
+ "text-left": { textAlign: "left" },
401
+ "text-center": { textAlign: "center" },
402
+ "text-right": { textAlign: "right" },
403
+ "text-justify": { textAlign: "justify" },
404
+ "text-start": { textAlign: "start" },
405
+ "text-end": { textAlign: "end" },
406
+ italic: { fontStyle: "italic" },
407
+ "not-italic": { fontStyle: "normal" },
408
+ uppercase: { textTransform: "uppercase" },
409
+ lowercase: { textTransform: "lowercase" },
410
+ capitalize: { textTransform: "capitalize" },
411
+ "normal-case": { textTransform: "none" },
412
+ underline: { textDecorationLine: "underline" },
413
+ overline: { textDecorationLine: "overline" },
414
+ "line-through": { textDecorationLine: "line-through" },
415
+ "no-underline": { textDecorationLine: "none" },
416
+ "whitespace-normal": { whiteSpace: "normal" },
417
+ "whitespace-nowrap": { whiteSpace: "nowrap" },
418
+ "whitespace-pre": { whiteSpace: "pre" },
419
+ "whitespace-pre-line": { whiteSpace: "pre-line" },
420
+ "whitespace-pre-wrap": { whiteSpace: "pre-wrap" },
421
+ "break-normal": { overflowWrap: "normal", wordBreak: "normal" },
422
+ "break-words": { overflowWrap: "break-word" },
423
+ "break-all": { wordBreak: "break-all" },
424
+ truncate: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
425
+ "overflow-auto": { overflow: "auto" },
426
+ "overflow-hidden": { overflow: "hidden" },
427
+ "overflow-clip": { overflow: "clip" },
428
+ "overflow-visible": { overflow: "visible" },
429
+ "overflow-scroll": { overflow: "scroll" },
430
+ "object-contain": { objectFit: "contain" },
431
+ "object-cover": { objectFit: "cover" },
432
+ "object-fill": { objectFit: "fill" },
433
+ "object-none": { objectFit: "none" },
434
+ "object-scale-down": { objectFit: "scale-down" },
435
+ "bg-fixed": { backgroundAttachment: "fixed" },
436
+ "bg-local": { backgroundAttachment: "local" },
437
+ "bg-scroll": { backgroundAttachment: "scroll" },
438
+ "bg-cover": { backgroundSize: "cover" },
439
+ "bg-contain": { backgroundSize: "contain" },
440
+ "bg-center": { backgroundPosition: "center" },
441
+ "bg-top": { backgroundPosition: "top" },
442
+ "bg-right": { backgroundPosition: "right" },
443
+ "bg-bottom": { backgroundPosition: "bottom" },
444
+ "bg-left": { backgroundPosition: "left" },
445
+ "border-solid": { borderStyle: "solid" },
446
+ "border-dashed": { borderStyle: "dashed" },
447
+ "border-dotted": { borderStyle: "dotted" },
448
+ "border-double": { borderStyle: "double" },
449
+ "border-hidden": { borderStyle: "hidden" },
450
+ "border-none": { borderStyle: "none" },
451
+ "rounded-none": { borderRadius: "0px" },
452
+ rounded: { borderRadius: "0.25rem" },
453
+ "rounded-sm": { borderRadius: "0.125rem" },
454
+ "rounded-md": { borderRadius: "0.375rem" },
455
+ "rounded-lg": { borderRadius: "0.5rem" },
456
+ "rounded-xl": { borderRadius: "0.75rem" },
457
+ "rounded-2xl": { borderRadius: "1rem" },
458
+ "rounded-3xl": { borderRadius: "1.5rem" },
459
+ "rounded-full": { borderRadius: "9999px" },
460
+ "shadow-sm": { boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
461
+ shadow: { boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)" },
462
+ "shadow-md": { boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" },
463
+ "shadow-lg": { boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" },
464
+ "shadow-xl": { boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" },
465
+ "shadow-2xl": { boxShadow: "0 25px 50px -12px rgb(0 0 0 / 0.25)" },
466
+ "shadow-inner": { boxShadow: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)" },
467
+ "shadow-none": { boxShadow: "0 0 #0000" },
468
+ "transition-none": { transitionProperty: "none" },
469
+ "transition-all": {
470
+ transitionProperty: "all",
471
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
472
+ transitionDuration: "150ms",
473
+ },
474
+ "transition-colors": {
475
+ transitionProperty:
476
+ "color, background-color, border-color, text-decoration-color, fill, stroke",
477
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
478
+ transitionDuration: "150ms",
479
+ },
480
+ "transition-opacity": {
481
+ transitionProperty: "opacity",
482
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
483
+ transitionDuration: "150ms",
484
+ },
485
+ "transition-shadow": {
486
+ transitionProperty: "box-shadow",
487
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
488
+ transitionDuration: "150ms",
489
+ },
490
+ "transition-transform": {
491
+ transitionProperty: "transform",
492
+ transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
493
+ transitionDuration: "150ms",
494
+ },
495
+ "cursor-auto": { cursor: "auto" },
496
+ "cursor-default": { cursor: "default" },
497
+ "cursor-pointer": { cursor: "pointer" },
498
+ "cursor-wait": { cursor: "wait" },
499
+ "cursor-text": { cursor: "text" },
500
+ "cursor-move": { cursor: "move" },
501
+ "cursor-not-allowed": { cursor: "not-allowed" },
502
+ "select-none": { userSelect: "none" },
503
+ "select-text": { userSelect: "text" },
504
+ "select-all": { userSelect: "all" },
505
+ "select-auto": { userSelect: "auto" },
506
+ visible: { visibility: "visible" },
507
+ invisible: { visibility: "hidden" },
508
+ collapse: { visibility: "collapse" },
509
+ "appearance-none": { appearance: "none" },
510
+ "pointer-events-none": { pointerEvents: "none" },
511
+ "pointer-events-auto": { pointerEvents: "auto" },
512
+ "resize-none": { resize: "none" },
513
+ "resize-y": { resize: "vertical" },
514
+ "resize-x": { resize: "horizontal" },
515
+ resize: { resize: "both" },
516
+ }
517
+
518
+ const FONT_SIZES: Readonly<Record<string, readonly [string, string]>> = {
519
+ xs: ["0.75rem", "1rem"],
520
+ sm: ["0.875rem", "1.25rem"],
521
+ base: ["1rem", "1.5rem"],
522
+ lg: ["1.125rem", "1.75rem"],
523
+ xl: ["1.25rem", "1.75rem"],
524
+ "2xl": ["1.5rem", "2rem"],
525
+ "3xl": ["1.875rem", "2.25rem"],
526
+ "4xl": ["2.25rem", "2.5rem"],
527
+ "5xl": ["3rem", "1"],
528
+ "6xl": ["3.75rem", "1"],
529
+ "7xl": ["4.5rem", "1"],
530
+ "8xl": ["6rem", "1"],
531
+ "9xl": ["8rem", "1"],
532
+ }
533
+
534
+ const FONT_WEIGHTS: Readonly<Record<string, number>> = {
535
+ thin: 100,
536
+ extralight: 200,
537
+ light: 300,
538
+ normal: 400,
539
+ medium: 500,
540
+ semibold: 600,
541
+ bold: 700,
542
+ extrabold: 800,
543
+ black: 900,
544
+ }
545
+
546
+ const LINE_HEIGHTS: Readonly<Record<string, string>> = {
547
+ none: "1",
548
+ tight: "1.25",
549
+ snug: "1.375",
550
+ normal: "1.5",
551
+ relaxed: "1.625",
552
+ loose: "2",
553
+ }
554
+
555
+ const MAX_WIDTHS: Readonly<Record<string, string>> = {
556
+ none: "none",
557
+ 0: "0rem",
558
+ xs: "20rem",
559
+ sm: "24rem",
560
+ md: "28rem",
561
+ lg: "32rem",
562
+ xl: "36rem",
563
+ "2xl": "42rem",
564
+ "3xl": "48rem",
565
+ "4xl": "56rem",
566
+ "5xl": "64rem",
567
+ "6xl": "72rem",
568
+ "7xl": "80rem",
569
+ full: "100%",
570
+ min: "min-content",
571
+ max: "max-content",
572
+ fit: "fit-content",
573
+ }
574
+
575
+ const PSEUDO_VARIANTS: Readonly<Record<string, string>> = {
576
+ hover: ":hover",
577
+ focus: ":focus",
578
+ "focus-visible": ":focus-visible",
579
+ active: ":active",
580
+ visited: ":visited",
581
+ target: ":target",
582
+ enabled: ":enabled",
583
+ disabled: ":disabled",
584
+ checked: ":checked",
585
+ indeterminate: ":indeterminate",
586
+ default: ":default",
587
+ required: ":required",
588
+ valid: ":valid",
589
+ invalid: ":invalid",
590
+ placeholder: "::placeholder",
591
+ "placeholder-shown": ":placeholder-shown",
592
+ autofill: ":autofill",
593
+ "read-only": ":read-only",
594
+ }
595
+
596
+ const CLASS_ATTRIBUTE =
597
+ /\bclassName\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)')\s*\}|\{([\s\S]*?)\})/g
598
+
599
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
600
+ typeof value === "object" && value !== null
601
+
602
+ const cloneStyle = (style: StyleObject): StyleObject => {
603
+ const result: Record<string, string | number | StyleObject> = {}
604
+ for (const [key, value] of Object.entries(style))
605
+ result[key] = isRecord(value) ? cloneStyle(value as StyleObject) : (value as string | number)
606
+ return result
607
+ }
608
+
609
+ const withVariants = (style: StyleObject, variants: readonly string[]): UtilityConversion => {
610
+ let result = cloneStyle(style)
611
+ for (const variant of [...variants].reverse()) {
612
+ const breakpoint = BREAKPOINTS[variant]
613
+ if (breakpoint !== undefined) {
614
+ result = { [`@media (min-width: ${breakpoint})`]: result }
615
+ continue
616
+ }
617
+ const pseudo = PSEUDO_VARIANTS[variant]
618
+ if (pseudo !== undefined) {
619
+ result = { [pseudo]: result }
620
+ continue
621
+ }
622
+ if (variant === "motion-safe") {
623
+ result = { "@media (prefers-reduced-motion: no-preference)": result }
624
+ continue
625
+ }
626
+ if (variant === "motion-reduce") {
627
+ result = { "@media (prefers-reduced-motion: reduce)": result }
628
+ continue
629
+ }
630
+ return {
631
+ reason: `variant '${variant}:' depends on parent state, arbitrary selectors, or runtime configuration`,
632
+ }
633
+ }
634
+ return { style: result }
635
+ }
636
+
637
+ const negativeValue = (value: string | number, negative: boolean): string | number => {
638
+ if (!negative) return value
639
+ if (typeof value === "number") return -value
640
+ if (value === "0") return 0
641
+ return value.startsWith("-") ? value.slice(1) : `-${value}`
642
+ }
643
+
644
+ const spacingValue = (value: string, negative: boolean): string | number | undefined => {
645
+ if (value === "auto") return negative ? undefined : "auto"
646
+ const spacing = SPACING[value]
647
+ return spacing === undefined ? undefined : negativeValue(spacing, negative)
648
+ }
649
+
650
+ const dimensionValue = (value: string, axis: "w" | "h"): string | number | undefined => {
651
+ if (value === "auto") return "auto"
652
+ if (value === "full") return "100%"
653
+ if (value === "screen") return axis === "h" ? "100vh" : "100vw"
654
+ if (value === "svw") return "100svw"
655
+ if (value === "lvw") return "100lvw"
656
+ if (value === "dvw") return "100dvw"
657
+ if (value === "svh") return "100svh"
658
+ if (value === "lvh") return "100lvh"
659
+ if (value === "dvh") return "100dvh"
660
+ if (value === "min") return "min-content"
661
+ if (value === "max") return "max-content"
662
+ if (value === "fit") return "fit-content"
663
+ return FRACTIONS[value] ?? SPACING[value]
664
+ }
665
+
666
+ const colorValue = (value: string): string | undefined => COLORS[value]
667
+
668
+ const directionalProperty = (
669
+ prefix: string,
670
+ axis: string | undefined,
671
+ ): readonly string[] | undefined => {
672
+ const properties: Readonly<Record<string, readonly string[]>> = {
673
+ p: ["padding"],
674
+ px: ["paddingInline"],
675
+ py: ["paddingBlock"],
676
+ pt: ["paddingTop"],
677
+ pr: ["paddingRight"],
678
+ pb: ["paddingBottom"],
679
+ pl: ["paddingLeft"],
680
+ ps: ["paddingInlineStart"],
681
+ pe: ["paddingInlineEnd"],
682
+ m: ["margin"],
683
+ mx: ["marginInline"],
684
+ my: ["marginBlock"],
685
+ mt: ["marginTop"],
686
+ mr: ["marginRight"],
687
+ mb: ["marginBottom"],
688
+ ml: ["marginLeft"],
689
+ ms: ["marginInlineStart"],
690
+ me: ["marginInlineEnd"],
691
+ inset: ["inset"],
692
+ "inset-x": ["insetInline"],
693
+ "inset-y": ["insetBlock"],
694
+ start: ["insetInlineStart"],
695
+ end: ["insetInlineEnd"],
696
+ top: ["top"],
697
+ right: ["right"],
698
+ bottom: ["bottom"],
699
+ left: ["left"],
700
+ }
701
+ if (axis === undefined) return properties[prefix]
702
+ return properties[`${prefix}-${axis}`]
703
+ }
704
+
705
+ const convertBase = (token: string, negative: boolean): UtilityConversion => {
706
+ const staticStyle = STATIC_STYLES[token]
707
+ if (staticStyle !== undefined) {
708
+ if (negative) return { reason: `negative utility '${token}' has no safe StyleX equivalent` }
709
+ return { style: staticStyle }
710
+ }
711
+
712
+ const spacing =
713
+ /^(p|px|py|pt|pr|pb|pl|ps|pe|m|mx|my|mt|mr|mb|ml|ms|me|inset|start|end|top|right|bottom|left)(?:-(.+))$/.exec(
714
+ token,
715
+ )
716
+ if (spacing !== null) {
717
+ const value = spacingValue(spacing[2] as string, negative)
718
+ const properties = directionalProperty(spacing[1] as string, undefined)
719
+ if (value !== undefined && properties !== undefined) {
720
+ return { style: Object.fromEntries(properties.map((property) => [property, value])) }
721
+ }
722
+ return { reason: `spacing value '${spacing[2]}' is not in the safe static scale` }
723
+ }
724
+
725
+ const gap = /^(gap|gap-x|gap-y)-(.+)$/.exec(token)
726
+ if (gap !== null) {
727
+ const value = spacingValue(gap[2] as string, negative)
728
+ if (value !== undefined) {
729
+ const property = gap[1] === "gap-x" ? "columnGap" : gap[1] === "gap-y" ? "rowGap" : "gap"
730
+ return { style: { [property]: value } }
731
+ }
732
+ return { reason: `gap value '${gap[2]}' is not in the safe static scale` }
733
+ }
734
+
735
+ const size = /^(w|h|min-w|min-h|max-w|max-h)-(.+)$/.exec(token)
736
+ if (size !== null) {
737
+ const rawValue = size[2] as string
738
+ const axis = size[1]?.endsWith("h") ? "h" : "w"
739
+ const value = size[1] === "max-w" ? MAX_WIDTHS[rawValue] : dimensionValue(rawValue, axis)
740
+ if (value !== undefined) {
741
+ const property =
742
+ size[1] === "w"
743
+ ? "width"
744
+ : size[1] === "h"
745
+ ? "height"
746
+ : size[1] === "min-w"
747
+ ? "minWidth"
748
+ : size[1] === "min-h"
749
+ ? "minHeight"
750
+ : size[1] === "max-w"
751
+ ? "maxWidth"
752
+ : "maxHeight"
753
+ return { style: { [property]: negativeValue(value, negative) } }
754
+ }
755
+ return { reason: `size value '${rawValue}' is not in the safe static scale` }
756
+ }
757
+
758
+ const inset = /^(inset|inset-x|inset-y|start|end|top|right|bottom|left)-(.+)$/.exec(token)
759
+ if (inset !== null) {
760
+ const value = spacingValue(inset[2] as string, negative)
761
+ const properties = directionalProperty(inset[1] as string, undefined)
762
+ if (value !== undefined && properties !== undefined)
763
+ return { style: Object.fromEntries(properties.map((property) => [property, value])) }
764
+ return { reason: `inset value '${inset[2]}' is not in the safe static scale` }
765
+ }
766
+
767
+ const zIndex = /^z-(.+)$/.exec(token)
768
+ if (zIndex !== null && (zIndex[1] === "auto" || /^\d+$/.test(zIndex[1] as string)))
769
+ return { style: { zIndex: zIndex[1] === "auto" ? "auto" : Number(zIndex[1]) } }
770
+
771
+ const order = /^order-(.+)$/.exec(token)
772
+ if (order !== null) {
773
+ const value = { first: -9999, last: 9999, none: 0 }[order[1] as "first" | "last" | "none"]
774
+ if (value !== undefined) return { style: { order: value } }
775
+ if (/^\d+$/.test(order[1] as string)) return { style: { order: Number(order[1]) } }
776
+ }
777
+
778
+ const textSize = /^text-(.+)$/.exec(token)
779
+ if (textSize !== null) {
780
+ const size = FONT_SIZES[textSize[1] as string]
781
+ if (size !== undefined) return { style: { fontSize: size[0], lineHeight: size[1] } }
782
+ const color = colorValue(textSize[1] as string)
783
+ if (color !== undefined) return { style: { color } }
784
+ return {
785
+ reason: `text utility '${textSize[1]}' is not in the safe static typography/color set`,
786
+ }
787
+ }
788
+
789
+ const background = /^bg-(.+)$/.exec(token)
790
+ if (background !== null) {
791
+ const color = colorValue(background[1] as string)
792
+ if (color !== undefined) return { style: { backgroundColor: color } }
793
+ return { reason: `background utility '${background[1]}' is not in the safe static color set` }
794
+ }
795
+
796
+ const font = /^font-(.+)$/.exec(token)
797
+ if (font !== null) {
798
+ const weight = FONT_WEIGHTS[font[1] as string]
799
+ if (weight !== undefined) return { style: { fontWeight: weight } }
800
+ if (font[1] === "sans") return { style: { fontFamily: "ui-sans-serif, system-ui, sans-serif" } }
801
+ if (font[1] === "serif") return { style: { fontFamily: "ui-serif, Georgia, serif" } }
802
+ if (font[1] === "mono")
803
+ return { style: { fontFamily: "ui-monospace, SFMono-Regular, monospace" } }
804
+ return { reason: `font utility '${font[1]}' is not in the safe static typography set` }
805
+ }
806
+
807
+ const leading = /^leading-(.+)$/.exec(token)
808
+ if (leading !== null) {
809
+ const value = LINE_HEIGHTS[leading[1] as string] ?? spacingValue(leading[1] as string, false)
810
+ if (value !== undefined) return { style: { lineHeight: value } }
811
+ return { reason: `line-height value '${leading[1]}' is not in the safe static scale` }
812
+ }
813
+
814
+ const tracking = /^tracking-(.+)$/.exec(token)
815
+ if (tracking !== null) {
816
+ const values: Readonly<Record<string, string>> = {
817
+ tighter: "-0.05em",
818
+ tight: "-0.025em",
819
+ normal: "0em",
820
+ wide: "0.025em",
821
+ wider: "0.05em",
822
+ widest: "0.1em",
823
+ }
824
+ const value = values[tracking[1] as string]
825
+ if (value !== undefined) return { style: { letterSpacing: value } }
826
+ return { reason: `letter-spacing value '${tracking[1]}' is not in the safe static set` }
827
+ }
828
+
829
+ const opacity = /^opacity-(\d+)$/.exec(token)
830
+ if (opacity !== null && Number(opacity[1]) <= 100)
831
+ return { style: { opacity: Number(opacity[1]) / 100 } }
832
+
833
+ const borderWidth = /^(border(?:-[trblxyse])?)(?:-(0|2|4|8))?$/.exec(token)
834
+ if (borderWidth !== null) {
835
+ const value = borderWidth[2] === undefined ? "1px" : `${borderWidth[2]}px`
836
+ const properties: Readonly<Record<string, string>> = {
837
+ border: "borderWidth",
838
+ "border-x": "borderInlineWidth",
839
+ "border-y": "borderBlockWidth",
840
+ "border-t": "borderTopWidth",
841
+ "border-r": "borderRightWidth",
842
+ "border-b": "borderBottomWidth",
843
+ "border-l": "borderLeftWidth",
844
+ "border-s": "borderInlineStartWidth",
845
+ "border-e": "borderInlineEndWidth",
846
+ }
847
+ const property = properties[borderWidth[1] as string]
848
+ if (property === undefined)
849
+ return { reason: `border utility '${token}' has no safe StyleX property mapping` }
850
+ return { style: { [property]: value } }
851
+ }
852
+
853
+ const borderColor = /^(border(?:-[trblxyse])?)-(.+)$/.exec(token)
854
+ if (borderColor !== null) {
855
+ const color = colorValue(borderColor[2] as string)
856
+ if (color !== undefined) {
857
+ const properties: Readonly<Record<string, string>> = {
858
+ border: "borderColor",
859
+ "border-x": "borderInlineColor",
860
+ "border-y": "borderBlockColor",
861
+ "border-t": "borderTopColor",
862
+ "border-r": "borderRightColor",
863
+ "border-b": "borderBottomColor",
864
+ "border-l": "borderLeftColor",
865
+ "border-s": "borderInlineStartColor",
866
+ "border-e": "borderInlineEndColor",
867
+ }
868
+ const property = properties[borderColor[1] as string]
869
+ if (property === undefined)
870
+ return { reason: `border utility '${token}' has no safe StyleX property mapping` }
871
+ return { style: { [property]: color } }
872
+ }
873
+ return { reason: `border color '${borderColor[2]}' is not in the safe static color set` }
874
+ }
875
+
876
+ const rounded = /^rounded(?:-([trblse]{1,2}))?(?:-(none|sm|md|lg|xl|2xl|3xl|full))?$/.exec(token)
877
+ if (rounded !== null) {
878
+ const sizes: Readonly<Record<string, string>> = {
879
+ none: "0px",
880
+ sm: "0.125rem",
881
+ md: "0.375rem",
882
+ lg: "0.5rem",
883
+ xl: "0.75rem",
884
+ "2xl": "1rem",
885
+ "3xl": "1.5rem",
886
+ full: "9999px",
887
+ }
888
+ const value = sizes[rounded[2] ?? "md"] ?? (rounded[2] === undefined ? "0.25rem" : undefined)
889
+ if (value !== undefined) {
890
+ const side = rounded[1]
891
+ if (side === undefined) return { style: { borderRadius: value } }
892
+ const properties: Readonly<Record<string, readonly string[]>> = {
893
+ t: ["borderTopLeftRadius", "borderTopRightRadius"],
894
+ r: ["borderTopRightRadius", "borderBottomRightRadius"],
895
+ b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
896
+ l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
897
+ s: ["borderStartStartRadius", "borderEndStartRadius"],
898
+ e: ["borderStartEndRadius", "borderEndEndRadius"],
899
+ }
900
+ const first = properties[side[0] as string]
901
+ const second = properties[side[1] as string]
902
+ if (first !== undefined && second !== undefined)
903
+ return {
904
+ style: Object.fromEntries([...first, ...second].map((property) => [property, value])),
905
+ }
906
+ if (first !== undefined)
907
+ return { style: Object.fromEntries(first.map((property) => [property, value])) }
908
+ }
909
+ }
910
+
911
+ return { reason: "utility is outside the safe static Tailwind-to-StyleX subset" }
912
+ }
913
+
914
+ const convertToken = (token: string): UtilityConversion => {
915
+ const parts = token.split(":")
916
+ const base = parts.pop() ?? ""
917
+ const negative = base.startsWith("-")
918
+ const normalized = negative ? base.slice(1) : base
919
+ const conversion = convertBase(normalized, negative)
920
+ if (conversion.style === undefined) return conversion
921
+ return withVariants(conversion.style, parts)
922
+ }
923
+
924
+ const lineOf = (source: string, offset: number): number => {
925
+ let line = 1
926
+ for (let index = 0; index < offset; index += 1) if (source[index] === "\n") line += 1
927
+ return line
928
+ }
929
+
930
+ const isLikelyJsxAttribute = (source: string, offset: number): boolean => {
931
+ const tagStart = source.lastIndexOf("<", offset)
932
+ const tagEnd = source.lastIndexOf(">", offset)
933
+ return tagStart > tagEnd && !source.slice(tagStart, offset).includes("<!--")
934
+ }
935
+
936
+ const classAttributes = (source: string): readonly ClassAttribute[] => {
937
+ const attributes: ClassAttribute[] = []
938
+ for (const match of source.matchAll(CLASS_ATTRIBUTE)) {
939
+ const start = match.index ?? 0
940
+ if (!isLikelyJsxAttribute(source, start)) continue
941
+ const end = start + match[0].length
942
+ const value = match[1] ?? match[2] ?? match[3] ?? match[4]
943
+ attributes.push(
944
+ value === undefined ? { start, end, expression: match[5] ?? "" } : { start, end, value },
945
+ )
946
+ }
947
+ return attributes
948
+ }
949
+
950
+ const safeIdentifier = (value: string): string => {
951
+ const normalized = value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^\d/, "_$&")
952
+ return normalized.length === 0 ? "utility" : normalized
953
+ }
954
+
955
+ const uniqueIdentifier = (source: string, base: string): string => {
956
+ const names = new Set(
957
+ [...source.matchAll(/\b(?:const|let|var|function|class|import)\s+([A-Za-z_$][\w$]*)/g)].map(
958
+ (match) => match[1],
959
+ ),
960
+ )
961
+ let candidate = base
962
+ let suffix = 2
963
+ while (names.has(candidate)) candidate = `${base}${suffix++}`
964
+ return candidate
965
+ }
966
+
967
+ const existingStylexImport = (source: string): string | undefined => {
968
+ const match =
969
+ /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["'](?:@stylexjs\/stylex|stylex)["']/.exec(
970
+ source,
971
+ )
972
+ return match?.[1]
973
+ }
974
+
975
+ const insertionOffset = (source: string): number => {
976
+ let offset = source.startsWith("#!") ? source.indexOf("\n") + 1 || source.length : 0
977
+ const lines = source.slice(offset).split(/(?<=\n)/)
978
+ let consumed = 0
979
+ let inImport = false
980
+ for (const line of lines) {
981
+ const trimmed = line.trim()
982
+ const directive = /^(['"])(?:use client|use server)\1;?$/.test(trimmed)
983
+ if (inImport || trimmed.startsWith("import ") || trimmed.startsWith("import{")) {
984
+ inImport = !/(?:from\s+["'][^"']+["']|import\s+["'][^"']+["'])\s*;?$/.test(trimmed)
985
+ consumed += line.length
986
+ continue
987
+ }
988
+ if (directive || trimmed === "") {
989
+ consumed += line.length
990
+ continue
991
+ }
992
+ break
993
+ }
994
+ offset += consumed
995
+ return offset
996
+ }
997
+
998
+ /** Transform one source file. The returned source is unchanged when every match is unsupported. */
999
+ export function migrateTailwindSource(source: string, file: string): SourceMigration {
1000
+ const attributes = classAttributes(source)
1001
+ if (attributes.length === 0) return { source, changed: false, replacements: 0, issues: [] }
1002
+
1003
+ const replacements: { start: number; end: number; text: string }[] = []
1004
+ const issues: MigrationIssue[] = []
1005
+ const styles = new Map<string, StyleObject>()
1006
+ const tokenToKey = new Map<string, string>()
1007
+
1008
+ for (const attribute of attributes) {
1009
+ if (attribute.value === undefined) {
1010
+ issues.push({
1011
+ file,
1012
+ line: lineOf(source, attribute.start),
1013
+ token: attribute.expression?.trim() || "className expression",
1014
+ reason: "dynamic className expressions are left for a manual StyleX props composition",
1015
+ })
1016
+ continue
1017
+ }
1018
+ const tokens = attribute.value.trim().split(/\s+/).filter(Boolean)
1019
+ if (tokens.length === 0) continue
1020
+ const converted: { token: string; style: StyleObject }[] = []
1021
+ let unsupported = false
1022
+ for (const token of tokens) {
1023
+ const conversion = convertToken(token)
1024
+ if (conversion.style === undefined) {
1025
+ issues.push({
1026
+ file,
1027
+ line: lineOf(source, attribute.start),
1028
+ token,
1029
+ reason: conversion.reason ?? "utility could not be converted safely",
1030
+ })
1031
+ unsupported = true
1032
+ } else converted.push({ token, style: conversion.style })
1033
+ }
1034
+ if (unsupported) continue
1035
+
1036
+ const refs: string[] = []
1037
+ for (const { token, style } of converted) {
1038
+ let key = tokenToKey.get(token)
1039
+ if (key === undefined) {
1040
+ const base = `tw_${safeIdentifier(token)}`
1041
+ key = base
1042
+ let suffix = 2
1043
+ while (styles.has(key)) key = `${base}_${suffix++}`
1044
+ tokenToKey.set(token, key)
1045
+ styles.set(key, style)
1046
+ }
1047
+ refs.push(`${stylesNamePlaceholder}.${key}`)
1048
+ }
1049
+ replacements.push({
1050
+ start: attribute.start,
1051
+ end: attribute.end,
1052
+ text: `{...${stylexNamePlaceholder}.props(${refs.join(", ")})}`,
1053
+ })
1054
+ }
1055
+
1056
+ if (replacements.length === 0) return { source, changed: false, replacements: 0, issues }
1057
+
1058
+ const stylexName = existingStylexImport(source) ?? uniqueIdentifier(source, "nifraStylex")
1059
+ const stylesName = uniqueIdentifier(source, "nifraStyles")
1060
+ const rewritten = [...replacements]
1061
+ .sort((left, right) => right.start - left.start)
1062
+ .reduce(
1063
+ (current, replacement) =>
1064
+ `${current.slice(0, replacement.start)}${replacement.text
1065
+ .replaceAll(stylexNamePlaceholder, stylexName)
1066
+ .replaceAll(stylesNamePlaceholder, stylesName)}${current.slice(replacement.end)}`,
1067
+ source,
1068
+ )
1069
+ const needsImport = existingStylexImport(source) === undefined
1070
+ const generated = `${needsImport ? `import * as ${stylexName} from "${STYLEX_SPECIFIER}"\n\n` : ""}const ${stylesName} = ${stylexName}.create(${JSON.stringify(Object.fromEntries(styles), null, 2)})\n\n`
1071
+ const offset = insertionOffset(rewritten)
1072
+ const withGenerated = `${rewritten.slice(0, offset)}${generated}${rewritten.slice(offset)}`
1073
+ return { source: withGenerated, changed: true, replacements: replacements.length, issues }
1074
+ }
1075
+
1076
+ const stylexNamePlaceholder = "__NIFRA_STYLEX__"
1077
+ const stylesNamePlaceholder = "__NIFRA_STYLES__"
1078
+ const STYLEX_SPECIFIER = "@stylexjs/" + "stylex"
1079
+
1080
+ async function sourceFiles(root: string): Promise<readonly string[]> {
1081
+ const files: string[] = []
1082
+ const visit = async (directory: string): Promise<void> => {
1083
+ const entries = await readdir(directory, { withFileTypes: true })
1084
+ for (const entry of entries) {
1085
+ if (entry.name.startsWith(".") && entry.isDirectory()) continue
1086
+ if (entry.isDirectory()) {
1087
+ if (!SKIP_DIRECTORIES.has(entry.name)) await visit(resolve(directory, entry.name))
1088
+ continue
1089
+ }
1090
+ if (entry.isFile() && SOURCE_FILE.test(entry.name)) files.push(resolve(directory, entry.name))
1091
+ }
1092
+ }
1093
+ await visit(root)
1094
+ return files.sort()
1095
+ }
1096
+
1097
+ /** Scan a project and optionally write only the statically safe transformations. */
1098
+ export async function migrateTailwindToStylex(
1099
+ root: string,
1100
+ options: StylexMigrationOptions = {},
1101
+ ): Promise<StylexMigrationResult> {
1102
+ const write = options.write === true
1103
+ const files: StylexMigrationFile[] = []
1104
+ const issues: MigrationIssue[] = []
1105
+ const changed: string[] = []
1106
+ const written: string[] = []
1107
+ const paths = await sourceFiles(root)
1108
+ for (const path of paths) {
1109
+ const file = relative(root, path).replaceAll("\\", "/")
1110
+ const source = await readFile(path, "utf8")
1111
+ const migration = migrateTailwindSource(source, file)
1112
+ if (!migration.changed && migration.issues.length === 0) continue
1113
+ const willWrite = write && migration.changed
1114
+ if (migration.changed) changed.push(file)
1115
+ if (willWrite) {
1116
+ await writeFile(path, migration.source, "utf8")
1117
+ written.push(file)
1118
+ }
1119
+ issues.push(...migration.issues)
1120
+ files.push({
1121
+ file,
1122
+ changed: migration.changed,
1123
+ replacements: migration.replacements,
1124
+ written: willWrite,
1125
+ issues: migration.issues,
1126
+ })
1127
+ }
1128
+ return {
1129
+ ok: issues.length === 0,
1130
+ from: "tailwind",
1131
+ to: "stylex",
1132
+ write,
1133
+ scanned: paths.length,
1134
+ changed,
1135
+ written,
1136
+ issues,
1137
+ files,
1138
+ }
1139
+ }